Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Commit

Permalink
web: add popover and alert for browser extension (#14256)
Browse files Browse the repository at this point in the history
Display an alert on first render of a repo page after the user has seen 3 hovers. Add a popover when the user first clicks on 'View on [code host]'.
  • Loading branch information
tjkandala authored and efritz committed Oct 9, 2020
1 parent 3d971c8 commit cdf8f6e
Show file tree
Hide file tree
Showing 59 changed files with 2,251 additions and 629 deletions.
1 change: 1 addition & 0 deletions client/branded/src/global-styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ $code-bg: var(--body-bg);
@import './card';
@import './dropdown';
@import './modal';
@import './popover';
@import './nav';
@import './type';
@import './list-group';
Expand Down
6 changes: 6 additions & 0 deletions client/branded/src/global-styles/popover.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
$popover-font-size: $font-size-base;
$popover-arrow-outer-color: var(--border-color);
$popover-arrow-color: var(--body-bg);
$popover-max-width: auto;

@import 'bootstrap/scss/popover';
6 changes: 3 additions & 3 deletions client/shared/src/actions/ActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { catchError, map, mapTo, mergeMap, startWith, tap } from 'rxjs/operators
import { ExecuteCommandParams } from '../api/client/services/command'
import { ActionContribution, Evaluated } from '../api/protocol'
import { urlForOpenPanel } from '../commands/commands'
import { LinkOrButton } from '../components/LinkOrButton'
import { ButtonLink } from '../components/LinkOrButton'
import { ExtensionsControllerProps } from '../extensions/controller'
import { PlatformContextProps } from '../platform/context'
import { TelemetryProps } from '../telemetry/telemetryService'
Expand Down Expand Up @@ -206,7 +206,7 @@ export class ActionItem extends React.PureComponent<ActionItemProps, State> {
: {}

return (
<LinkOrButton
<ButtonLink
data-tooltip={
this.props.showInlineError && isErrorLike(this.state.actionOrError)
? `Error: ${this.state.actionOrError.message}`
Expand Down Expand Up @@ -237,7 +237,7 @@ export class ActionItem extends React.PureComponent<ActionItemProps, State> {
<LoadingSpinner className={this.props.iconClassName} />
</div>
)}
</LinkOrButton>
</ButtonLink>
)
}

Expand Down
6 changes: 3 additions & 3 deletions client/shared/src/components/LinkOrButton.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React from 'react'
import renderer from 'react-test-renderer'
import { LinkOrButton } from './LinkOrButton'
import { ButtonLink } from './LinkOrButton'

describe('LinkOrButton', () => {
test('render a link when "to" is set', () => {
const component = renderer.create(<LinkOrButton to="http://example.com">foo</LinkOrButton>)
const component = renderer.create(<ButtonLink to="http://example.com">foo</ButtonLink>)
expect(component.toJSON()).toMatchSnapshot()
})

test('render a button when "to" is undefined', () => {
const component = renderer.create(<LinkOrButton to={undefined}>foo</LinkOrButton>)
const component = renderer.create(<ButtonLink to={undefined}>foo</ButtonLink>)
expect(component.toJSON()).toMatchSnapshot()
})
})
9 changes: 6 additions & 3 deletions client/shared/src/components/LinkOrButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,17 @@ interface Props extends Pick<AnchorHTMLAttributes<never>, 'target' | 'rel'> {
className?: string

disabled?: boolean

id?: string
}

/**
* A component that is displayed in the same way, regardless of whether it's a link (with a
* destination URL) or a button (with a click handler).
*
* It is keyboard accessible: unlike <Link> or <a>, pressing the enter key triggers it. Unlike
* <button>, it shows a focus ring.
* It is keyboard accessible: unlike `<Link>` or `<a>`, pressing the enter key triggers it.
*/
export const LinkOrButton: React.FunctionComponent<Props> = ({
export const ButtonLink: React.FunctionComponent<Props> = ({
className = 'nav-link',
to,
target,
Expand All @@ -53,6 +54,7 @@ export const LinkOrButton: React.FunctionComponent<Props> = ({
'data-tooltip': tooltip,
onSelect = noop,
children,
id,
}) => {
// We need to set up a keypress listener because <a onclick> doesn't get
// triggered by enter.
Expand All @@ -76,6 +78,7 @@ export const LinkOrButton: React.FunctionComponent<Props> = ({
tabIndex: 0,
onClick: onSelect,
onKeyPress: onAnchorKeyPress,
id,
}

const onClickPreventDefault: React.MouseEventHandler<HTMLAnchorElement> = useCallback(
Expand Down
12 changes: 11 additions & 1 deletion client/web/src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import React, { Suspense } from 'react'
import React, { Suspense, useCallback } from 'react'
import { Redirect, Route, RouteComponentProps, Switch, matchPath } from 'react-router'
import { Observable } from 'rxjs'
import { ActivationProps } from '../../shared/src/components/activation/Activation'
Expand Down Expand Up @@ -64,6 +64,7 @@ import { AuthenticatedUser, authRequired as authRequiredObservable } from './aut
import { SearchPatternType } from './graphql-operations'
import { TelemetryProps } from '../../shared/src/telemetry/telemetryService'
import { useObservable } from '../../shared/src/util/useObservable'
import { useExtensionAlertAnimation } from './nav/UserNavItem'

export interface LayoutProps
extends RouteComponentProps<{}>,
Expand Down Expand Up @@ -169,6 +170,13 @@ export const Layout: React.FunctionComponent<LayoutProps> = props => {

const breadcrumbProps = useBreadcrumbs()

// Control browser extension discoverability animation here.
// `Layout` is the lowest common ancestor of `UserNavItem` (target) and `RepoContainer` (trigger)
const { isExtensionAlertAnimating, startExtensionAlertAnimation } = useExtensionAlertAnimation()
const onExtensionAlertDismissed = useCallback(() => {
startExtensionAlertAnimation()
}, [startExtensionAlertAnimation])

useScrollToLocationHash(props.location)
// Remove trailing slash (which is never valid in any of our URLs).
if (props.location.pathname !== '/' && props.location.pathname.endsWith('/')) {
Expand All @@ -178,6 +186,7 @@ export const Layout: React.FunctionComponent<LayoutProps> = props => {
const context = {
...props,
...breadcrumbProps,
onExtensionAlertDismissed,
}

return (
Expand Down Expand Up @@ -211,6 +220,7 @@ export const Layout: React.FunctionComponent<LayoutProps> = props => {
}
hideNavLinks={false}
minimalNavLinks={minimalNavLinks}
isExtensionAlertAnimating={isExtensionAlertAnimating}
/>
)}
{needsSiteInit && !isSiteInit && <Redirect to="/site-admin/init" />}
Expand Down
2 changes: 2 additions & 0 deletions client/web/src/SourcegraphWebApp.scss
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ body,
@import './repo/RepoContainer';
@import './repo/FilePathBreadcrumbs.scss';
@import './repo/settings/RepoSettingsArea';
@import './repo/actions//InstallBrowserExtensionPopover.scss';
@import './repo/actions/InstallBrowserExtensionAlert.scss';
@import './components/LoaderInput';
@import '../../branded/src/components/CodeSnippet';
@import './components/PageHeader';
Expand Down
2 changes: 1 addition & 1 deletion client/web/src/auth/icons.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'

export const SourcegraphIcon: React.FunctionComponent<React.SVGAttributes<SVGSVGElement>> = props => (
<svg width="65" height="64" viewBox="0 0 65 64" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<svg viewBox="0 0 65 64" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
Expand Down
16 changes: 16 additions & 0 deletions client/web/src/components/Dialog.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@import '~@reach/dialog/styles.css';

.modal-body {
background-color: var(--body-bg);
padding: 1rem;
width: 32rem;
max-width: 100vw;

&--centered {
transform: translate(-50%, -50%);
position: absolute;
top: 50%;
margin: 0;
left: 50%;
}
}
23 changes: 0 additions & 23 deletions client/web/src/components/ModalContainer.scss

This file was deleted.

44 changes: 0 additions & 44 deletions client/web/src/components/ModalContainer.story.tsx

This file was deleted.

93 changes: 0 additions & 93 deletions client/web/src/components/ModalContainer.tsx

This file was deleted.

17 changes: 15 additions & 2 deletions client/web/src/components/shared.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import classNames from 'classnames'
import React, { useCallback } from 'react'
import React, { useCallback, useEffect } from 'react'
import { ActionsNavItems, ActionsNavItemsProps } from '../../../shared/src/actions/ActionsNavItems'
import { CommandListPopoverButton, CommandListPopoverButtonProps } from '../../../shared/src/commandPalette/CommandList'
import {
Expand All @@ -9,10 +9,11 @@ import {
import { isErrorLike } from '../../../shared/src/util/errors'
import { HoverOverlay, HoverOverlayProps } from '../../../shared/src/hover/HoverOverlay'
import { useLocalStorage } from '../util/useLocalStorage'
import { HoverThresholdProps } from '../repo/RepoContainer'

// Components from shared with web-styling class names applied

export const WebHoverOverlay: React.FunctionComponent<HoverOverlayProps> = props => {
export const WebHoverOverlay: React.FunctionComponent<HoverOverlayProps & HoverThresholdProps> = props => {
const [dismissedAlerts, setDismissedAlerts] = useLocalStorage<string[]>('WebHoverOverlay.dismissedAlerts', [])
const onAlertDismissed = useCallback(
(alertType: string) => {
Expand All @@ -31,6 +32,18 @@ export const WebHoverOverlay: React.FunctionComponent<HoverOverlayProps> = props
propsToUse = { ...props, hoverOrError: { ...props.hoverOrError, alerts: filteredAlerts } }
}

const { hoverOrError } = propsToUse
const { onHoverShown, hoveredToken } = props

/** Whether the hover has actual content (that provides value to the user) */
const hoverHasValue = hoverOrError !== 'loading' && !isErrorLike(hoverOrError) && !!hoverOrError?.contents?.length

useEffect(() => {
if (hoverHasValue) {
onHoverShown?.()
}
}, [hoveredToken?.filePath, hoveredToken?.line, hoveredToken?.character, onHoverShown, hoverHasValue])

return (
<HoverOverlay
{...propsToUse}
Expand Down
Loading

0 comments on commit cdf8f6e

Please sign in to comment.