mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 20:51:43 +00:00
[Wildcard] New Tooltip Component (#35870)
* Stub in basic example Tooltip compound component * Add new Wildcard Tooltip using Radix, basic Storybook story * Simplify Tooltip API, add Wildcard styles * Updates from PR feedback * Finalize Tooltip tests, ESLint rule * Additional PR feedback, support conditional tooltips * Fix existing Tooltip imports * Remove old ESLint rule disabling import of Tooltip component from Wildcard * Update which Tooltip component is used in main JetBrains app file
This commit is contained in:
parent
0c0e3af871
commit
feb3429727
11
.eslintrc.js
11
.eslintrc.js
@ -59,12 +59,6 @@ const config = {
|
||||
importNames: ['Form'],
|
||||
message: 'Use the <Form /> component from @sourcegraph/branded package instead',
|
||||
},
|
||||
{
|
||||
name: '@sourcegraph/wildcard',
|
||||
importNames: ['Tooltip'],
|
||||
message:
|
||||
'Please ensure there is only a single `<Tooltip />` component present in the React tree. To display a specific tooltip, you can add the `data-tooltip` attribute to the relevant element.',
|
||||
},
|
||||
{
|
||||
name: 'zustand',
|
||||
importNames: ['default'],
|
||||
@ -181,6 +175,11 @@ See https://handbook.sourcegraph.com/community/faq#is-all-of-sourcegraph-open-so
|
||||
message:
|
||||
'Consider using useTemporarySetting instead of useLocalStorage so settings are synced when users log in elsewhere. More info at https://docs.sourcegraph.com/dev/background-information/web/temporary_settings',
|
||||
},
|
||||
{
|
||||
selector: 'JSXAttribute JSXIdentifier[name="data-tooltip"]',
|
||||
message:
|
||||
'The use of data-tooltip has been deprecated. Please wrap your trigger element with the <Tooltip> component from Wildcard instead. If there are problems using the new <Tooltip>, please contact the Frontend Platform Team.',
|
||||
},
|
||||
],
|
||||
// https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#eslint
|
||||
'react/jsx-uses-react': 'off',
|
||||
|
||||
@ -6,7 +6,7 @@ import { ThemeProps } from '@sourcegraph/shared/src/theme'
|
||||
import { MockedStoryProvider, MockedStoryProviderProps, usePrependStyles, useTheme } from '@sourcegraph/storybook'
|
||||
// Add root Tooltip for Storybook
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Tooltip, WildcardThemeContext } from '@sourcegraph/wildcard'
|
||||
import { DeprecatedTooltip, WildcardThemeContext } from '@sourcegraph/wildcard'
|
||||
|
||||
import brandedStyles from '../global-styles/index.scss'
|
||||
|
||||
@ -33,7 +33,7 @@ export const BrandedStory: React.FunctionComponent<React.PropsWithChildren<Brand
|
||||
<MockedStoryProvider mocks={mocks} useStrictMocking={useStrictMocking}>
|
||||
<WildcardThemeContext.Provider value={{ isBranded: true }}>
|
||||
<MemoryRouter {...memoryRouterProps}>
|
||||
<Tooltip />
|
||||
<DeprecatedTooltip />
|
||||
<Children isLightTheme={isLightTheme} />
|
||||
</MemoryRouter>
|
||||
</WildcardThemeContext.Provider>
|
||||
|
||||
@ -24,8 +24,7 @@ import { fetchStreamSuggestions } from '@sourcegraph/shared/src/search/suggestio
|
||||
import { EMPTY_SETTINGS_CASCADE, SettingsCascadeOrError } from '@sourcegraph/shared/src/settings/settings'
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
// Add root Tooltip for JetBrains
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useObservable, WildcardThemeContext, Tooltip } from '@sourcegraph/wildcard'
|
||||
import { useObservable, WildcardThemeContext, DeprecatedTooltip } from '@sourcegraph/wildcard'
|
||||
|
||||
import { getAuthenticatedUser } from '../sourcegraph-api-access/api-gateway'
|
||||
import { initializeSourcegraphSettings } from '../sourcegraphSettings'
|
||||
@ -203,7 +202,7 @@ export const App: React.FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
|
||||
return (
|
||||
<WildcardThemeContext.Provider value={{ isBranded: true }}>
|
||||
<Tooltip />
|
||||
<DeprecatedTooltip />
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div className={styles.root} onMouseDown={preventAll}>
|
||||
<div className={styles.searchBoxContainer}>
|
||||
|
||||
@ -8,7 +8,7 @@ import { Observable, merge, of } from 'rxjs'
|
||||
import { tap, switchMapTo, startWith, delay } from 'rxjs/operators'
|
||||
|
||||
import { KeyboardShortcut } from '@sourcegraph/shared/src/keyboardShortcuts'
|
||||
import { Button, Icon, TooltipController, useEventObservable } from '@sourcegraph/wildcard'
|
||||
import { Button, Icon, DeprecatedTooltipController, useEventObservable } from '@sourcegraph/wildcard'
|
||||
|
||||
interface Props {
|
||||
fullQuery: string
|
||||
@ -36,7 +36,7 @@ export const CopyQueryButton: React.FunctionComponent<React.PropsWithChildren<Pr
|
||||
clicks.pipe(
|
||||
tap(copyFullQuery),
|
||||
switchMapTo(merge(of(true), of(false).pipe(delay(2000)))),
|
||||
tap(() => TooltipController.forceUpdate()),
|
||||
tap(() => DeprecatedTooltipController.forceUpdate()),
|
||||
startWith(false)
|
||||
),
|
||||
[copyFullQuery]
|
||||
|
||||
10
client/shared/dev/mockDomRect.ts
Normal file
10
client/shared/dev/mockDomRect.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/* eslint-disable unicorn/prevent-abbreviations */
|
||||
|
||||
// JSDOM does not have support for DOMRect, needed for tooltips.
|
||||
// https://github.com/radix-ui/primitives/issues/420#issuecomment-771615182
|
||||
if ('DOMRect' in window === false) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
window.DOMRect = {
|
||||
fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }),
|
||||
} as any
|
||||
}
|
||||
@ -51,8 +51,7 @@ import { TemporarySettingsStorage } from '@sourcegraph/shared/src/settings/tempo
|
||||
import { globbingEnabledFromSettings } from '@sourcegraph/shared/src/util/globbing'
|
||||
import {
|
||||
// This is the root Tooltip usage
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
Tooltip,
|
||||
DeprecatedTooltip,
|
||||
FeedbackText,
|
||||
setLinkComponent,
|
||||
RouterLink,
|
||||
@ -468,7 +467,7 @@ export class SourcegraphWebApp extends React.Component<SourcegraphWebAppProps, S
|
||||
<BrowserExtensionTracker />
|
||||
</Router>
|
||||
</ScrollManager>
|
||||
<Tooltip key={1} />
|
||||
<DeprecatedTooltip key={1} />
|
||||
<Notifications
|
||||
key={2}
|
||||
extensionsController={this.extensionsController}
|
||||
|
||||
@ -7,7 +7,7 @@ import { ThemeProps } from '@sourcegraph/shared/src/theme'
|
||||
import { MockedStoryProvider, MockedStoryProviderProps, usePrependStyles, useTheme } from '@sourcegraph/storybook'
|
||||
// Add root Tooltip for Storybook
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Tooltip, WildcardThemeContext } from '@sourcegraph/wildcard'
|
||||
import { DeprecatedTooltip, WildcardThemeContext } from '@sourcegraph/wildcard'
|
||||
|
||||
import { BreadcrumbSetters, BreadcrumbsProps, useBreadcrumbs } from './Breadcrumbs'
|
||||
|
||||
@ -41,7 +41,7 @@ export const WebStory: React.FunctionComponent<React.PropsWithChildren<WebStoryP
|
||||
<MockedStoryProvider mocks={mocks} useStrictMocking={useStrictMocking}>
|
||||
<WildcardThemeContext.Provider value={{ isBranded: true }}>
|
||||
<MemoryRouter {...memoryRouterProps}>
|
||||
<Tooltip />
|
||||
<DeprecatedTooltip />
|
||||
<Children
|
||||
{...breadcrumbSetters}
|
||||
isLightTheme={isLightTheme}
|
||||
|
||||
@ -4,7 +4,7 @@ import copy from 'copy-to-clipboard'
|
||||
import { merge, Observable, of } from 'rxjs'
|
||||
import { delay, startWith, switchMapTo, tap } from 'rxjs/operators'
|
||||
|
||||
import { TooltipController, useEventObservable } from '@sourcegraph/wildcard'
|
||||
import { DeprecatedTooltipController, useEventObservable } from '@sourcegraph/wildcard'
|
||||
|
||||
type URLValue = string | undefined
|
||||
type useCopiedHandlerReturn = [(value?: URLValue) => void, boolean | undefined]
|
||||
@ -24,7 +24,7 @@ export function useCopyURLHandler(): useCopiedHandlerReturn {
|
||||
clicks.pipe(
|
||||
tap(copyDashboardURL),
|
||||
switchMapTo(merge(of(true), of(false).pipe(delay(2000)))),
|
||||
tap(() => TooltipController.forceUpdate()),
|
||||
tap(() => DeprecatedTooltipController.forceUpdate()),
|
||||
startWith(false)
|
||||
),
|
||||
[copyDashboardURL]
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
TooltipController,
|
||||
DeprecatedTooltipController,
|
||||
Icon,
|
||||
Link,
|
||||
ProductStatusBadge,
|
||||
@ -208,7 +208,7 @@ const QueryPanel: React.FunctionComponent<React.PropsWithChildren<QueryPanelProp
|
||||
setTimeout(() => setCurrentCopyTooltip(copyTooltip), 1000)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
TooltipController.forceUpdate()
|
||||
DeprecatedTooltipController.forceUpdate()
|
||||
})
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
@ -9,8 +9,7 @@ import { FlatExtensionHostAPI } from '@sourcegraph/shared/src/api/contract'
|
||||
import { pretendProxySubscribable, pretendRemote } from '@sourcegraph/shared/src/api/util'
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { extensionsController, NOOP_PLATFORM_CONTEXT } from '@sourcegraph/shared/src/testing/searchTestHelpers'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Tooltip } from '@sourcegraph/wildcard'
|
||||
import { DeprecatedTooltip } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AppRouterContainer } from '../../components/AppRouterContainer'
|
||||
|
||||
@ -63,7 +62,7 @@ const decorator: DecoratorFn = story => (
|
||||
<>
|
||||
<style>{webStyles}</style>
|
||||
<AppRouterContainer>
|
||||
<Tooltip />
|
||||
<DeprecatedTooltip />
|
||||
<div className="container mt-3">{story()}</div>
|
||||
</AppRouterContainer>
|
||||
</>
|
||||
|
||||
@ -17,7 +17,7 @@ import {
|
||||
RenderModeSpec,
|
||||
UIRangeSpec,
|
||||
} from '@sourcegraph/shared/src/util/url'
|
||||
import { TooltipController } from '@sourcegraph/wildcard'
|
||||
import { DeprecatedTooltipController } from '@sourcegraph/wildcard'
|
||||
|
||||
import { getWebGraphQLClient, requestGraphQL } from '../backend/graphql'
|
||||
import { eventLogger } from '../tracking/eventLogger'
|
||||
@ -73,7 +73,7 @@ export function createPlatformContext(): PlatformContext {
|
||||
},
|
||||
getGraphQLClient: getWebGraphQLClient,
|
||||
requestGraphQL: ({ request, variables }) => requestGraphQL(request, variables),
|
||||
forceUpdateTooltip: () => TooltipController.forceUpdate(),
|
||||
forceUpdateTooltip: () => DeprecatedTooltipController.forceUpdate(),
|
||||
createExtensionHost: async () =>
|
||||
(await import('@sourcegraph/shared/src/api/extension/worker')).createExtensionHost(),
|
||||
urlToFile: toPrettyWebBlobURL,
|
||||
|
||||
@ -4,7 +4,7 @@ import copy from 'copy-to-clipboard'
|
||||
import ContentCopyIcon from 'mdi-react/ContentCopyIcon'
|
||||
import { useLocation } from 'react-router'
|
||||
|
||||
import { Button, TooltipController, Icon, screenReaderAnnounce } from '@sourcegraph/wildcard'
|
||||
import { Button, DeprecatedTooltipController, Icon, screenReaderAnnounce } from '@sourcegraph/wildcard'
|
||||
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
import { parseBrowserRepoURL } from '../../util/url'
|
||||
@ -19,7 +19,7 @@ export const CopyPathAction: React.FunctionComponent<React.PropsWithChildren<unk
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
TooltipController.forceUpdate()
|
||||
DeprecatedTooltipController.forceUpdate()
|
||||
}, [copied])
|
||||
|
||||
const onClick = (event: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
toViewStateHash,
|
||||
} from '@sourcegraph/common'
|
||||
import { parseQueryAndHash } from '@sourcegraph/shared/src/util/url'
|
||||
import { TooltipController, Icon } from '@sourcegraph/wildcard'
|
||||
import { DeprecatedTooltipController, Icon } from '@sourcegraph/wildcard'
|
||||
|
||||
import { eventLogger } from '../../../tracking/eventLogger'
|
||||
import { RepoHeaderActionButtonLink } from '../../components/RepoHeaderActions'
|
||||
@ -65,7 +65,7 @@ export class ToggleHistoryPanel extends React.PureComponent<
|
||||
const visible = ToggleHistoryPanel.isVisible(this.props.location)
|
||||
eventLogger.log(visible ? 'HideHistoryPanel' : 'ShowHistoryPanel')
|
||||
this.props.history.push(ToggleHistoryPanel.locationWithVisibility(this.props.location, !visible))
|
||||
TooltipController.forceUpdate()
|
||||
DeprecatedTooltipController.forceUpdate()
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { fromEvent, Subject, Subscription } from 'rxjs'
|
||||
import { filter } from 'rxjs/operators'
|
||||
|
||||
import { WrapDisabledIcon } from '@sourcegraph/shared/src/components/icons'
|
||||
import { TooltipController, Icon } from '@sourcegraph/wildcard'
|
||||
import { DeprecatedTooltipController, Icon } from '@sourcegraph/wildcard'
|
||||
|
||||
import { eventLogger } from '../../../tracking/eventLogger'
|
||||
import { RepoHeaderActionButtonLink } from '../../components/RepoHeaderActions'
|
||||
@ -52,7 +52,7 @@ export class ToggleLineWrap extends React.PureComponent<
|
||||
ToggleLineWrap.setValue(value)
|
||||
this.setState({ value })
|
||||
this.props.onDidUpdate(value)
|
||||
TooltipController.forceUpdate()
|
||||
DeprecatedTooltipController.forceUpdate()
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import EyeIcon from 'mdi-react/EyeIcon'
|
||||
import { useLocation } from 'react-router'
|
||||
|
||||
import { RenderMode } from '@sourcegraph/shared/src/util/url'
|
||||
import { TooltipController, Icon } from '@sourcegraph/wildcard'
|
||||
import { DeprecatedTooltipController, Icon } from '@sourcegraph/wildcard'
|
||||
|
||||
import { RepoHeaderActionButtonLink } from '../../components/RepoHeaderActions'
|
||||
import { RepoHeaderContext } from '../../RepoHeader'
|
||||
@ -33,7 +33,7 @@ export const ToggleRenderedFileMode: React.FunctionComponent<React.PropsWithChil
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
TooltipController.forceUpdate()
|
||||
DeprecatedTooltipController.forceUpdate()
|
||||
}, [mode])
|
||||
|
||||
if (actionType === 'dropdown') {
|
||||
|
||||
@ -7,7 +7,7 @@ import DotsHorizontalIcon from 'mdi-react/DotsHorizontalIcon'
|
||||
import FileDocumentIcon from 'mdi-react/FileDocumentIcon'
|
||||
|
||||
import { pluralize } from '@sourcegraph/common'
|
||||
import { Button, ButtonGroup, TooltipController, Link, Icon, Typography } from '@sourcegraph/wildcard'
|
||||
import { Button, ButtonGroup, DeprecatedTooltipController, Link, Icon, Typography } from '@sourcegraph/wildcard'
|
||||
|
||||
import { Timestamp } from '../../components/time/Timestamp'
|
||||
import { GitCommitFields } from '../../graphql-operations'
|
||||
@ -89,7 +89,7 @@ export const GitCommitNode: React.FunctionComponent<React.PropsWithChildren<GitC
|
||||
}, [showCommitMessageBody])
|
||||
|
||||
useEffect(() => {
|
||||
TooltipController.forceUpdate()
|
||||
DeprecatedTooltipController.forceUpdate()
|
||||
}, [flashCopiedToClipboardMessage])
|
||||
|
||||
const copyToClipboard = useCallback((oid): void => {
|
||||
|
||||
@ -1,167 +1,33 @@
|
||||
// Base class
|
||||
.tooltip {
|
||||
--tooltip-opacity: 1;
|
||||
--tooltip-font-size: 0.75rem;
|
||||
--tooltip-zindex: 1070;
|
||||
--tooltip-arrow-width: 0.8rem;
|
||||
--tooltip-arrow-height: 0.4rem;
|
||||
--tooltip-bg: var(--gray-08);
|
||||
--tooltip-arrow-color: var(--tooltip-bg);
|
||||
:root {
|
||||
--tooltip-font-size: 0.75rem; // 12px
|
||||
--tooltip-line-height: 1.02rem; // 16.32px / 16px, per Figma
|
||||
--tooltip-max-width: 256px;
|
||||
--tooltip-color: var(--white);
|
||||
--tooltip-color: var(--light-text);
|
||||
--tooltip-border-radius: var(--border-radius);
|
||||
--tooltip-padding-y: 0.25rem;
|
||||
--tooltip-padding-x: 0.5rem;
|
||||
--tooltip-margin: 0;
|
||||
}
|
||||
|
||||
position: absolute;
|
||||
z-index: var(--tooltip-zindex);
|
||||
display: block;
|
||||
margin: var(--tooltip-margin);
|
||||
.tooltip {
|
||||
// This goes along with the fix described here: https://github.com/radix-ui/primitives/issues/620#issuecomment-1079147761
|
||||
// This display style keeps the tooltip aligned on Button components, since `inline` or `inline-block` did not fill the full height,
|
||||
// resulting in tooltips anchoring on just the Button text, not the full Button element
|
||||
display: inline-flex;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
font-size: var(--tooltip-font-size);
|
||||
|
||||
// Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.
|
||||
// So reset our font and text properties to avoid inheriting weird values.
|
||||
// rules from reset-text() mixin
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-align: left; // Fallback for where `start` is not supported
|
||||
text-align: start;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-break: normal;
|
||||
word-spacing: normal;
|
||||
line-break: auto;
|
||||
|
||||
// Allow breaking very long words so they don't overflow the tooltip's bounds
|
||||
word-wrap: break-word;
|
||||
opacity: 0;
|
||||
|
||||
// Allows line breaks in tooltips
|
||||
white-space: pre-wrap;
|
||||
animation: 0.25s fade-in;
|
||||
pointer-events: none;
|
||||
line-height: 1rem;
|
||||
|
||||
&.show {
|
||||
opacity: var(--tooltip-opacity);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: var(--tooltip-arrow-width);
|
||||
height: var(--tooltip-arrow-height);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
height: 0.8rem; // Fix bug in Firefox where the arrow won't inherit the parent's height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-top {
|
||||
padding: var(--tooltip-arrow-height) 0;
|
||||
|
||||
.arrow {
|
||||
bottom: 0;
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
border-width: var(--tooltip-arrow-height) calc(var(--tooltip-arrow-width) / 2) 0;
|
||||
border-top-color: var(--tooltip-arrow-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-right {
|
||||
padding: 0 var(--tooltip-arrow-height);
|
||||
|
||||
.arrow {
|
||||
left: 0;
|
||||
width: var(--tooltip-arrow-height);
|
||||
height: var(--tooltip-arrow-width);
|
||||
|
||||
&::before {
|
||||
right: 0;
|
||||
border-width: calc(var(--tooltip-arrow-width) / 2) var(--tooltip-arrow-height)
|
||||
calc(var(--tooltip-arrow-width) / 2) 0;
|
||||
border-right-color: var(--tooltip-arrow-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-bottom {
|
||||
padding: var(--tooltip-arrow-height) 0;
|
||||
|
||||
.arrow {
|
||||
top: 0;
|
||||
|
||||
&::before {
|
||||
bottom: 0;
|
||||
border-width: 0 calc(var(--tooltip-arrow-width) / 2) var(--tooltip-arrow-height);
|
||||
border-bottom-color: var(--tooltip-arrow-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-left {
|
||||
padding: 0 var(--tooltip-arrow-height);
|
||||
|
||||
.arrow {
|
||||
right: 0;
|
||||
width: var(--tooltip-arrow-height);
|
||||
height: var(--tooltip-arrow-width);
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
border-width: calc(var(--tooltip-arrow-width) / 2) 0 calc(var(--tooltip-arrow-width) / 2)
|
||||
var(--tooltip-arrow-height);
|
||||
border-left-color: var(--tooltip-arrow-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-auto {
|
||||
&[x-placement^='top'] {
|
||||
@extend .tooltip-top;
|
||||
}
|
||||
&[x-placement^='right'] {
|
||||
@extend .tooltip-right;
|
||||
}
|
||||
&[x-placement^='bottom'] {
|
||||
@extend .tooltip-bottom;
|
||||
}
|
||||
&[x-placement^='left'] {
|
||||
@extend .tooltip-left;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for the tooltip content
|
||||
.tooltip-inner {
|
||||
line-height: var(--tooltip-line-height);
|
||||
max-width: var(--tooltip-max-width);
|
||||
padding: var(--tooltip-padding-y) var(--tooltip-padding-x);
|
||||
color: var(--tooltip-color);
|
||||
text-align: center;
|
||||
background-color: var(--tooltip-bg);
|
||||
border-radius: var(--tooltip-border-radius);
|
||||
color: var(--tooltip-color);
|
||||
padding: var(--tooltip-padding-y) var(--tooltip-padding-x);
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
.tooltip-arrow {
|
||||
fill: var(--tooltip-bg);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { DecoratorFn, Meta, Story } from '@storybook/react'
|
||||
|
||||
@ -7,10 +7,8 @@ import webStyles from '@sourcegraph/web/src/SourcegraphWebApp.scss'
|
||||
|
||||
import { Button, Grid, Typography, Text } from '..'
|
||||
|
||||
import { Tooltip } from './Tooltip'
|
||||
import { TooltipController } from './TooltipController'
|
||||
import { Tooltip } from '.'
|
||||
|
||||
// BrandedStory already renders `<Tooltip />` so in Stories we don't render `<Tooltip />`
|
||||
const decorator: DecoratorFn = story => (
|
||||
<BrandedStory styles={webStyles}>{() => <div className="p-5">{story()}</div>}</BrandedStory>
|
||||
)
|
||||
@ -40,141 +38,168 @@ const config: Meta = {
|
||||
export default config
|
||||
|
||||
export const Basic: Story = () => (
|
||||
<>
|
||||
<Text>
|
||||
You can <strong data-tooltip="Tooltip 1">hover me</strong> or <strong data-tooltip="Tooltip 2">me</strong>.
|
||||
</Text>
|
||||
</>
|
||||
<Text>
|
||||
You can{' '}
|
||||
<Tooltip content="Tooltip 1">
|
||||
<strong>hover me</strong>
|
||||
</Tooltip>{' '}
|
||||
or{' '}
|
||||
<Tooltip content="Tooltip 2">
|
||||
<strong>me</strong>
|
||||
</Tooltip>
|
||||
.
|
||||
</Text>
|
||||
)
|
||||
|
||||
Basic.parameters = {
|
||||
chromatic: {
|
||||
disable: true,
|
||||
},
|
||||
}
|
||||
export const Conditional: Story = () => {
|
||||
const [clicked, setClicked] = useState<boolean>(false)
|
||||
|
||||
export const Positions: Story = () => (
|
||||
<>
|
||||
<Typography.H1>Tooltip</Typography.H1>
|
||||
<Typography.H2>Positions</Typography.H2>
|
||||
|
||||
<Grid columnCount={4}>
|
||||
<div>
|
||||
<Button variant="secondary" data-placement="top" data-tooltip="Tooltip on top">
|
||||
Tooltip on top
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="secondary" data-placement="bottom" data-tooltip="Tooltip on bottom">
|
||||
Tooltip on bottom
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="secondary" data-placement="left" data-tooltip="Tooltip on left">
|
||||
Tooltip on left
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="secondary" data-placement="right" data-tooltip="Tooltip on right">
|
||||
Tooltip on right
|
||||
</Button>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<Typography.H2>Max width</Typography.H2>
|
||||
<Grid columnCount={1}>
|
||||
<div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
data-tooltip="Nulla porttitor accumsan tincidunt. Proin eget tortor risus. Quisque velit nisi, pretium ut lacinia in, elementum id enim. Donec rutrum congue leo eget malesuada."
|
||||
>
|
||||
Tooltip with long text
|
||||
</Button>
|
||||
</div>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
|
||||
Positions.parameters = {
|
||||
chromatic: {
|
||||
disable: true,
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
If you take a look at the handleEvent function in useTooltipState, you can see that the listeners are being added to the 'document',
|
||||
which means any 'mouseover/click' event will cause the tooltip to disappear.
|
||||
*/
|
||||
export const Pinned: Story = () => {
|
||||
const clickElement = useCallback((element: HTMLElement | null) => {
|
||||
if (element) {
|
||||
// The tooltip takes some time to set-up.
|
||||
// hence we need to delay the click by some ms.
|
||||
setTimeout(() => {
|
||||
element.click()
|
||||
}, 10)
|
||||
}
|
||||
}, [])
|
||||
function onClick() {
|
||||
setClicked(true)
|
||||
setTimeout(() => setClicked(false), 1500)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span data-tooltip="My tooltip" ref={clickElement}>
|
||||
Example
|
||||
</span>
|
||||
<Grid columnCount={1}>
|
||||
<div>
|
||||
<Tooltip content={clicked ? "Now there's a Tooltip!" : null}>
|
||||
<Button variant="primary" onClick={onClick}>
|
||||
Click Me to See a Tooltip!
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Text>
|
||||
<small>
|
||||
(A pinned tooltip is shown when the target element is rendered, without any user interaction
|
||||
needed.)
|
||||
</small>
|
||||
A Tooltip can be conditionally shown by alternating between passing{' '}
|
||||
<Typography.Code>null</Typography.Code> and a <Typography.Code>string</Typography.Code> in as{' '}
|
||||
<Typography.Code>content</Typography.Code>.
|
||||
</Text>
|
||||
</>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
Pinned.parameters = {
|
||||
export const DisabledTrigger: Story = () => (
|
||||
<Grid columnCount={1}>
|
||||
<div>
|
||||
<Tooltip content="Tooltip still works properly">
|
||||
<Button variant="primary" disabled={true} style={{ pointerEvents: 'none' }}>
|
||||
Disabled Button 🚫
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Text>
|
||||
{/**
|
||||
* This is necessary to support our current implementation using Radix.
|
||||
* Reference: https://www.radix-ui.com/docs/primitives/components/tooltip#displaying-a-tooltip-from-a-disabled-button
|
||||
* */}
|
||||
When rendering a Tooltip for a disabled <Typography.Code>{'<Button>'}</Typography.Code>, the button element
|
||||
also needs to have the CSS property <Typography.Code>pointer-events: none</Typography.Code>.
|
||||
</Text>
|
||||
</Grid>
|
||||
)
|
||||
|
||||
export const LongContent: Story = () => (
|
||||
<Grid columnCount={1}>
|
||||
<div>
|
||||
<Tooltip content="Nulla porttitor accumsan tincidunt. Proin eget tortor risus. Quisque velit nisi, pretium ut lacinia in, elementum id enim. Donec rutrum congue leo eget malesuada.">
|
||||
<Button variant="primary">Example</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Text>
|
||||
Tooltips with long text will not exceed the width specified by{' '}
|
||||
<Typography.Code>--tooltip-max-width</Typography.Code>.
|
||||
</Text>
|
||||
</Grid>
|
||||
)
|
||||
|
||||
export const DefaultOpen: Story = () => (
|
||||
<Grid columnCount={1}>
|
||||
<div>
|
||||
<Tooltip content="Click me!" defaultOpen={true}>
|
||||
<Button variant="primary">Example</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Text>
|
||||
A pinned tooltip is shown on initial render (no user input required) by setting{' '}
|
||||
<Typography.Code>defaultOpen={'{true}'}</Typography.Code>.
|
||||
</Text>
|
||||
</Grid>
|
||||
)
|
||||
|
||||
DefaultOpen.storyName = 'Default Open (Pinned)'
|
||||
DefaultOpen.parameters = {
|
||||
chromatic: {
|
||||
// Chromatic pauses CSS animations by default and resets them to their initial state
|
||||
pauseAnimationAtEnd: true,
|
||||
enableDarkMode: true,
|
||||
disableSnapshot: false,
|
||||
},
|
||||
}
|
||||
|
||||
const ForceUpdateTooltip = () => {
|
||||
const [copied, setCopied] = useState<boolean>(false)
|
||||
export const PlacementOptions: Story = () => (
|
||||
<>
|
||||
<Grid columnCount={5}>
|
||||
<div>
|
||||
<Tooltip content="Tooltip on top" placement="top">
|
||||
<Button variant="primary">top</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
useEffect(() => {
|
||||
TooltipController.forceUpdate()
|
||||
}, [copied])
|
||||
<div>
|
||||
<Tooltip content="Tooltip on right" placement="right">
|
||||
<Button variant="primary">right</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
const onClick: React.MouseEventHandler<HTMLButtonElement> = event => {
|
||||
event.preventDefault()
|
||||
<div>
|
||||
<Tooltip content="Tooltip on bottom" placement="bottom">
|
||||
<Button variant="primary">bottom</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
setCopied(true)
|
||||
<div>
|
||||
<Tooltip content="Tooltip on left" placement="left">
|
||||
<Button variant="primary">left</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 1500)
|
||||
<div>
|
||||
<Tooltip content="Default Tooltip placement">
|
||||
<Button variant="primary">(default)</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<Text>
|
||||
The Tooltip will use the specified <Typography.Code>placement</Typography.Code> unless a viewport collision
|
||||
is detected, in which case it will be mirrored.
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
|
||||
export const UpdateContent: Story = () => {
|
||||
const [clicked, setClicked] = useState<boolean>(false)
|
||||
|
||||
function onClick() {
|
||||
setClicked(true)
|
||||
setTimeout(() => setClicked(false), 1500)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography.H2>
|
||||
Force update tooltip with <Typography.Code>TooltipController.forceUpdate()</Typography.Code>
|
||||
</Typography.H2>
|
||||
<Grid columnCount={1}>
|
||||
<div>
|
||||
<Tooltip content={clicked ? 'New message!' : 'Click to change the message.'}>
|
||||
<Button variant="primary" onClick={onClick}>
|
||||
Click Me
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Text>
|
||||
<Button variant="primary" onClick={onClick} data-tooltip={copied ? 'Copied!' : 'Click to copy'}>
|
||||
Button
|
||||
</Button>
|
||||
The string passed in as <Typography.Code>content</Typography.Code> can be modified without any
|
||||
controlled or forced updates required.
|
||||
</Text>
|
||||
</>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export const Controller: Story = () => <ForceUpdateTooltip />
|
||||
|
||||
Controller.parameters = {
|
||||
chromatic: {
|
||||
disable: true,
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,60 +1,85 @@
|
||||
import { render, RenderResult, cleanup, fireEvent, waitFor, screen } from '@testing-library/react'
|
||||
import { render, RenderResult, cleanup, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import { Tooltip } from './Tooltip'
|
||||
|
||||
const TooltipTest = () => (
|
||||
<>
|
||||
<Tooltip />
|
||||
<div>
|
||||
Hover on{' '}
|
||||
<strong data-testid="hoverable-1" data-tooltip="Tooltip 1">
|
||||
me
|
||||
</strong>
|
||||
, or{' '}
|
||||
<strong data-testid="hoverable-2" data-tooltip="Tooltip 2">
|
||||
me
|
||||
</strong>
|
||||
</div>
|
||||
Hover on{' '}
|
||||
<Tooltip content="Tooltip 1">
|
||||
<strong data-testid="trigger-1">me</strong>
|
||||
</Tooltip>
|
||||
, or{' '}
|
||||
<Tooltip content="Tooltip 2">
|
||||
<strong data-testid="trigger-2">me</strong>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
|
||||
describe('Tooltip', () => {
|
||||
let queries: RenderResult
|
||||
let rendered: RenderResult
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('Hoverable Tooltip', () => {
|
||||
beforeEach(() => {
|
||||
queries = render(<TooltipTest />)
|
||||
beforeEach(() => {
|
||||
rendered = render(<TooltipTest />)
|
||||
})
|
||||
|
||||
it('displays content when the trigger is hovered', async () => {
|
||||
userEvent.hover(rendered.getByTestId('trigger-1'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(rendered.getByTestId('trigger-1').parentElement).toHaveAttribute('aria-describedby', 'radix-0')
|
||||
expect(rendered.getByTestId('trigger-2').parentElement).not.toHaveAttribute('aria-describedby')
|
||||
|
||||
// Should be one tooltip for visual users, and a second for use with aria-describedby
|
||||
const tooltips = rendered.getAllByRole('tooltip')
|
||||
expect(tooltips).toHaveLength(2)
|
||||
expect(tooltips[0]).toHaveTextContent('Tooltip 1')
|
||||
expect(tooltips[1]).toHaveTextContent('Tooltip 1')
|
||||
expect(tooltips[1]).toHaveAttribute('id', 'radix-0')
|
||||
})
|
||||
|
||||
it('Shows tooltip properly on hover', async () => {
|
||||
fireEvent.mouseOver(queries.getByTestId('hoverable-1'))
|
||||
userEvent.hover(rendered.getByTestId('trigger-2'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent('Tooltip 1')
|
||||
expect(screen.getByRole('tooltip').closest('.show.fade')).toBeInTheDocument()
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(rendered.getByTestId('trigger-1').parentElement).not.toHaveAttribute('aria-describedby')
|
||||
expect(rendered.getByTestId('trigger-2').parentElement).toHaveAttribute('aria-describedby', 'radix-1')
|
||||
|
||||
expect(document.body).toMatchSnapshot()
|
||||
// Should be one tooltip for visual users, and a second for use with aria-describedby
|
||||
const tooltips = rendered.getAllByRole('tooltip')
|
||||
expect(tooltips).toHaveLength(2)
|
||||
expect(tooltips[0]).toHaveTextContent('Tooltip 2')
|
||||
expect(tooltips[1]).toHaveTextContent('Tooltip 2')
|
||||
expect(tooltips[1]).toHaveAttribute('id', 'radix-1')
|
||||
})
|
||||
})
|
||||
|
||||
it('hides content when the ESC key is pressed', async () => {
|
||||
userEvent.hover(rendered.getByTestId('trigger-1'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(rendered.getAllByRole('tooltip')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('Handles multiple tooltips properly', async () => {
|
||||
fireEvent.mouseOver(queries.getByTestId('hoverable-1'))
|
||||
userEvent.type(rendered.getByTestId('trigger-1'), '{esc}')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent('Tooltip 1')
|
||||
expect(screen.getByRole('tooltip').closest('.show.fade')).toBeInTheDocument()
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(rendered.queryByRole('tooltip')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
fireEvent.mouseOver(queries.getByTestId('hoverable-2'))
|
||||
it('does not hide content when the trigger is clicked', async () => {
|
||||
userEvent.hover(rendered.getByTestId('trigger-1'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent('Tooltip 2')
|
||||
expect(screen.getByRole('tooltip').closest('.show.fade')).toBeInTheDocument()
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(rendered.getAllByRole('tooltip')).toHaveLength(2)
|
||||
})
|
||||
|
||||
expect(document.body).toMatchSnapshot()
|
||||
userEvent.click(rendered.getByTestId('trigger-1'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(rendered.getAllByRole('tooltip')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,63 +1,81 @@
|
||||
import React, { ReactNode, useMemo } from 'react'
|
||||
import React, { ReactNode } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import Popper from 'popper.js'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Tooltip as BootstrapTooltip } from 'reactstrap'
|
||||
|
||||
import { useTooltipState } from './useTooltipState'
|
||||
import { getTooltipStyle } from './utils'
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||
|
||||
import styles from './Tooltip.module.scss'
|
||||
|
||||
interface TooltipProps {
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
/** A single child element that will trigger the Tooltip to open on hover. */
|
||||
children: ReactNode
|
||||
/** The text that will be displayed in the Tooltip. If `null`, no Tooltip will be rendered, allowing for Tooltips to be shown conditionally. */
|
||||
content: string | null
|
||||
/** The open state of the tooltip when it is initially rendered. Defaults to `false`. */
|
||||
defaultOpen?: boolean
|
||||
/** The preferred side of the trigger to render against when open. Will be reversed if a collision is detected. Defaults to `right`. */
|
||||
placement?: TooltipPrimitive.TooltipContentProps['side']
|
||||
}
|
||||
|
||||
const TOOLTIP_MODIFIERS: Popper.Modifiers = {
|
||||
flip: {
|
||||
enabled: false,
|
||||
},
|
||||
preventOverflow: {
|
||||
boundariesElement: 'window',
|
||||
},
|
||||
/** Arrow width in pixels */
|
||||
const TOOLTIP_ARROW_WIDTH = 14
|
||||
/** Arrow height in pixel */
|
||||
const TOOLTIP_ARROW_HEIGHT = 6
|
||||
|
||||
// Handling the onPointerDownOutside event and preventing the default behavior allows us to keep the Tooltip content open
|
||||
// even if the trigger <span> was clicked; this allows buttons to be clicked and text to be selected without dismissing content.
|
||||
// Reference: https://github.com/radix-ui/primitives/issues/1077
|
||||
function onPointerDownOutside(event: Event): void {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a Tooltip that can be positioned relative to a target element.
|
||||
* Renders a Tooltip that will be positioned relative to the wrapped child element. Please reference the examples in Storybook
|
||||
* for more details on specific use cases.
|
||||
*
|
||||
* This component should typically only need to be rendered once in a React tree.
|
||||
* If you need to attach a tooltip to an specific element, simply add the `data-tooltip` attribute to that element.
|
||||
* To support accessibility, our tooltips should:
|
||||
* - Be supplemental to the user journey, not essential.
|
||||
* - Use clear and concise text.
|
||||
* - Not include interactive content (you probably want a `<Popover>` instead).
|
||||
*
|
||||
* Related accessibility documentation: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tooltip_role
|
||||
*/
|
||||
export const Tooltip: React.FunctionComponent<React.PropsWithChildren<TooltipProps>> = ({ className }) => {
|
||||
const { subject, content, subjectSeq, placement = 'auto', delay } = useTooltipState()
|
||||
export const Tooltip: React.FunctionComponent<TooltipProps> = ({
|
||||
children,
|
||||
content,
|
||||
defaultOpen = false,
|
||||
placement = 'right',
|
||||
}) => (
|
||||
// NOTE: We plan to consolidate this logic with our Popover component in the future, but chose Radix first to support short-term accessibility needs.
|
||||
// GitHub issue: https://github.com/sourcegraph/sourcegraph/issues/36080
|
||||
<TooltipPrimitive.Root delayDuration={0} defaultOpen={defaultOpen}>
|
||||
<TooltipPrimitive.Trigger asChild={true}>
|
||||
{/** The onClick and role attributes here are part of the onPointerDownOutside fix described above. */}
|
||||
<span role="presentation" className={styles.tooltip} onClick={event => event.preventDefault()}>
|
||||
{children}
|
||||
|
||||
const tooltipStyle = useMemo(() => getTooltipStyle(placement), [placement])
|
||||
{
|
||||
// The rest of the Tooltip components still need to be rendered for the content to correctly be shown conditionally.
|
||||
content === null ? null : (
|
||||
/*
|
||||
* Rendering the Content within the Trigger is a workaround to support being able to hover over the Tooltip content itself.
|
||||
* Refrence: https://github.com/radix-ui/primitives/issues/620#issuecomment-1079147761
|
||||
*/
|
||||
<TooltipPrimitive.TooltipContent
|
||||
onPointerDownOutside={onPointerDownOutside}
|
||||
className={styles.tooltipContent}
|
||||
side={placement}
|
||||
role="tooltip"
|
||||
>
|
||||
{content}
|
||||
|
||||
if (!subject || !content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapTooltip
|
||||
// Set key prop to work around a bug where quickly mousing between 2 elements with tooltips
|
||||
// displays the 2nd element's tooltip as still pointing to the first.
|
||||
key={subjectSeq}
|
||||
isOpen={true}
|
||||
target={subject}
|
||||
placement={placement}
|
||||
// in order to add our own placement classes we need to set the popperClassNames
|
||||
// here is where bootstrap injects it's placement classes such as 'bs-tooltip-auto' automatically.
|
||||
popperClassName={classNames(styles.tooltip, styles.show, className, tooltipStyle)}
|
||||
arrowClassName={styles.arrow}
|
||||
innerClassName={styles.tooltipInner}
|
||||
// This is a workaround to an issue with tooltips in reactstrap that causes the entire page to freeze.
|
||||
// Remove when https://github.com/reactstrap/reactstrap/issues/1482 is fixed.
|
||||
modifiers={TOOLTIP_MODIFIERS}
|
||||
delay={delay}
|
||||
>
|
||||
{content}
|
||||
</BootstrapTooltip>
|
||||
)
|
||||
}
|
||||
<TooltipPrimitive.Arrow
|
||||
className={styles.tooltipArrow}
|
||||
height={TOOLTIP_ARROW_HEIGHT}
|
||||
width={TOOLTIP_ARROW_WIDTH}
|
||||
/>
|
||||
</TooltipPrimitive.TooltipContent>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
</TooltipPrimitive.Trigger>
|
||||
</TooltipPrimitive.Root>
|
||||
)
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export * from './Tooltip'
|
||||
export * from './TooltipController'
|
||||
|
||||
@ -0,0 +1,167 @@
|
||||
// Base class
|
||||
.tooltip {
|
||||
--tooltip-opacity: 1;
|
||||
--tooltip-font-size: 0.75rem;
|
||||
--tooltip-zindex: 1070;
|
||||
--tooltip-arrow-width: 0.8rem;
|
||||
--tooltip-arrow-height: 0.4rem;
|
||||
--tooltip-bg: var(--gray-08);
|
||||
--tooltip-arrow-color: var(--tooltip-bg);
|
||||
--tooltip-max-width: 256px;
|
||||
--tooltip-color: var(--white);
|
||||
--tooltip-border-radius: var(--border-radius);
|
||||
--tooltip-padding-y: 0.25rem;
|
||||
--tooltip-padding-x: 0.5rem;
|
||||
--tooltip-margin: 0;
|
||||
|
||||
position: absolute;
|
||||
z-index: var(--tooltip-zindex);
|
||||
display: block;
|
||||
margin: var(--tooltip-margin);
|
||||
font-size: var(--tooltip-font-size);
|
||||
|
||||
// Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.
|
||||
// So reset our font and text properties to avoid inheriting weird values.
|
||||
// rules from reset-text() mixin
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-align: left; // Fallback for where `start` is not supported
|
||||
text-align: start;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-break: normal;
|
||||
word-spacing: normal;
|
||||
line-break: auto;
|
||||
|
||||
// Allow breaking very long words so they don't overflow the tooltip's bounds
|
||||
word-wrap: break-word;
|
||||
opacity: 0;
|
||||
|
||||
// Allows line breaks in tooltips
|
||||
white-space: pre-wrap;
|
||||
animation: 0.25s fade-in;
|
||||
pointer-events: none;
|
||||
line-height: 1rem;
|
||||
|
||||
&.show {
|
||||
opacity: var(--tooltip-opacity);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: var(--tooltip-arrow-width);
|
||||
height: var(--tooltip-arrow-height);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
height: 0.8rem; // Fix bug in Firefox where the arrow won't inherit the parent's height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-top {
|
||||
padding: var(--tooltip-arrow-height) 0;
|
||||
|
||||
.arrow {
|
||||
bottom: 0;
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
border-width: var(--tooltip-arrow-height) calc(var(--tooltip-arrow-width) / 2) 0;
|
||||
border-top-color: var(--tooltip-arrow-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-right {
|
||||
padding: 0 var(--tooltip-arrow-height);
|
||||
|
||||
.arrow {
|
||||
left: 0;
|
||||
width: var(--tooltip-arrow-height);
|
||||
height: var(--tooltip-arrow-width);
|
||||
|
||||
&::before {
|
||||
right: 0;
|
||||
border-width: calc(var(--tooltip-arrow-width) / 2) var(--tooltip-arrow-height)
|
||||
calc(var(--tooltip-arrow-width) / 2) 0;
|
||||
border-right-color: var(--tooltip-arrow-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-bottom {
|
||||
padding: var(--tooltip-arrow-height) 0;
|
||||
|
||||
.arrow {
|
||||
top: 0;
|
||||
|
||||
&::before {
|
||||
bottom: 0;
|
||||
border-width: 0 calc(var(--tooltip-arrow-width) / 2) var(--tooltip-arrow-height);
|
||||
border-bottom-color: var(--tooltip-arrow-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-left {
|
||||
padding: 0 var(--tooltip-arrow-height);
|
||||
|
||||
.arrow {
|
||||
right: 0;
|
||||
width: var(--tooltip-arrow-height);
|
||||
height: var(--tooltip-arrow-width);
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
border-width: calc(var(--tooltip-arrow-width) / 2) 0 calc(var(--tooltip-arrow-width) / 2)
|
||||
var(--tooltip-arrow-height);
|
||||
border-left-color: var(--tooltip-arrow-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-auto {
|
||||
&[x-placement^='top'] {
|
||||
@extend .tooltip-top;
|
||||
}
|
||||
&[x-placement^='right'] {
|
||||
@extend .tooltip-right;
|
||||
}
|
||||
&[x-placement^='bottom'] {
|
||||
@extend .tooltip-bottom;
|
||||
}
|
||||
&[x-placement^='left'] {
|
||||
@extend .tooltip-left;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for the tooltip content
|
||||
.tooltip-inner {
|
||||
max-width: var(--tooltip-max-width);
|
||||
padding: var(--tooltip-padding-y) var(--tooltip-padding-x);
|
||||
color: var(--tooltip-color);
|
||||
text-align: center;
|
||||
background-color: var(--tooltip-bg);
|
||||
border-radius: var(--tooltip-border-radius);
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,181 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { DecoratorFn, Meta, Story } from '@storybook/react'
|
||||
|
||||
import { BrandedStory } from '@sourcegraph/branded/src/components/BrandedStory'
|
||||
import webStyles from '@sourcegraph/web/src/SourcegraphWebApp.scss'
|
||||
|
||||
import { Button, Grid, Typography, Text } from '../..'
|
||||
|
||||
import { TooltipController } from './TooltipController'
|
||||
|
||||
// BrandedStory already renders `<Tooltip />` so in Stories we don't render `<Tooltip />`
|
||||
const decorator: DecoratorFn = story => (
|
||||
<BrandedStory styles={webStyles}>{() => <div className="p-5">{story()}</div>}</BrandedStory>
|
||||
)
|
||||
|
||||
const config: Meta = {
|
||||
title: 'wildcard/Tooltip/Deprecated',
|
||||
|
||||
decorators: [decorator],
|
||||
|
||||
parameters: {
|
||||
component: 'DeprecatedTooltip',
|
||||
design: [
|
||||
{
|
||||
type: 'figma',
|
||||
name: 'Figma Light',
|
||||
url: 'https://www.figma.com/file/NIsN34NH7lPu04olBzddTw/Wildcard-Design-System?node-id=3131%3A38534',
|
||||
},
|
||||
{
|
||||
type: 'figma',
|
||||
name: 'Figma Dark',
|
||||
url: 'https://www.figma.com/file/NIsN34NH7lPu04olBzddTw/Wildcard-Design-System?node-id=3131%3A38727',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
export const Basic: Story = () => (
|
||||
<Text>
|
||||
You can{' '}
|
||||
<strong data-tooltip="Tooltip 1" data-placement="right">
|
||||
hover me
|
||||
</strong>{' '}
|
||||
or <strong data-tooltip="Tooltip 2">me</strong>.
|
||||
</Text>
|
||||
)
|
||||
|
||||
Basic.parameters = {
|
||||
chromatic: {
|
||||
disable: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Positions: Story = () => (
|
||||
<>
|
||||
<Typography.H1>Tooltip</Typography.H1>
|
||||
<Typography.H2>Positions</Typography.H2>
|
||||
|
||||
<Grid columnCount={4}>
|
||||
<div>
|
||||
<Button variant="secondary" data-placement="top" data-tooltip="Tooltip on top">
|
||||
Tooltip on top
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="secondary" data-placement="bottom" data-tooltip="Tooltip on bottom">
|
||||
Tooltip on bottom
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="secondary" data-placement="left" data-tooltip="Tooltip on left">
|
||||
Tooltip on left
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="secondary" data-placement="right" data-tooltip="Tooltip on right">
|
||||
Tooltip on right
|
||||
</Button>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<Typography.H2>Max width</Typography.H2>
|
||||
<Grid columnCount={1}>
|
||||
<div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
data-tooltip="Nulla porttitor accumsan tincidunt. Proin eget tortor risus. Quisque velit nisi, pretium ut lacinia in, elementum id enim. Donec rutrum congue leo eget malesuada."
|
||||
>
|
||||
Tooltip with long text
|
||||
</Button>
|
||||
</div>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
|
||||
Positions.parameters = {
|
||||
chromatic: {
|
||||
disable: true,
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
If you take a look at the handleEvent function in useTooltipState, you can see that the listeners are being added to the 'document',
|
||||
which means any 'mouseover/click' event will cause the tooltip to disappear.
|
||||
*/
|
||||
export const Pinned: Story = () => {
|
||||
const clickElement = useCallback((element: HTMLElement | null) => {
|
||||
if (element) {
|
||||
// The tooltip takes some time to set-up.
|
||||
// hence we need to delay the click by some ms.
|
||||
setTimeout(() => {
|
||||
element.click()
|
||||
}, 10)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<span data-tooltip="My tooltip" ref={clickElement}>
|
||||
Example
|
||||
</span>
|
||||
<Text>
|
||||
<small>
|
||||
(A pinned tooltip is shown when the target element is rendered, without any user interaction
|
||||
needed.)
|
||||
</small>
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Pinned.parameters = {
|
||||
chromatic: {
|
||||
// Chromatic pauses CSS animations by default and resets them to their initial state
|
||||
pauseAnimationAtEnd: true,
|
||||
enableDarkMode: true,
|
||||
disableSnapshot: false,
|
||||
},
|
||||
}
|
||||
|
||||
const ForceUpdateTooltip = () => {
|
||||
const [copied, setCopied] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
TooltipController.forceUpdate()
|
||||
}, [copied])
|
||||
|
||||
const onClick: React.MouseEventHandler<HTMLButtonElement> = event => {
|
||||
event.preventDefault()
|
||||
|
||||
setCopied(true)
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography.H2>
|
||||
Force update tooltip with <Typography.Code>TooltipController.forceUpdate()</Typography.Code>
|
||||
</Typography.H2>
|
||||
<Text>
|
||||
<Button variant="primary" onClick={onClick} data-tooltip={copied ? 'Copied!' : 'Click to copy'}>
|
||||
Button
|
||||
</Button>
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const Controller: Story = () => <ForceUpdateTooltip />
|
||||
|
||||
Controller.parameters = {
|
||||
chromatic: {
|
||||
disable: true,
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import { render, RenderResult, cleanup, fireEvent, waitFor, screen } from '@testing-library/react'
|
||||
|
||||
import { Tooltip } from './Tooltip'
|
||||
|
||||
const TooltipTest = () => (
|
||||
<>
|
||||
<Tooltip />
|
||||
<div>
|
||||
Hover on{' '}
|
||||
<strong data-testid="hoverable-1" data-tooltip="Tooltip 1">
|
||||
me
|
||||
</strong>
|
||||
, or{' '}
|
||||
<strong data-testid="hoverable-2" data-tooltip="Tooltip 2">
|
||||
me
|
||||
</strong>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
describe('Tooltip', () => {
|
||||
let queries: RenderResult
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('Hoverable Tooltip', () => {
|
||||
beforeEach(() => {
|
||||
queries = render(<TooltipTest />)
|
||||
})
|
||||
|
||||
it('Shows tooltip properly on hover', async () => {
|
||||
fireEvent.mouseOver(queries.getByTestId('hoverable-1'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent('Tooltip 1')
|
||||
expect(screen.getByRole('tooltip').closest('.show.fade')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(document.body).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('Handles multiple tooltips properly', async () => {
|
||||
fireEvent.mouseOver(queries.getByTestId('hoverable-1'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent('Tooltip 1')
|
||||
expect(screen.getByRole('tooltip').closest('.show.fade')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.mouseOver(queries.getByTestId('hoverable-2'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent('Tooltip 2')
|
||||
expect(screen.getByRole('tooltip').closest('.show.fade')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(document.body).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,67 @@
|
||||
import React, { ReactNode, useMemo } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import Popper from 'popper.js'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Tooltip as BootstrapTooltip } from 'reactstrap'
|
||||
|
||||
import { useTooltipState } from './useTooltipState'
|
||||
import { getTooltipStyle } from './utils'
|
||||
|
||||
import styles from './Tooltip.module.scss'
|
||||
|
||||
interface TooltipProps {
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const TOOLTIP_MODIFIERS: Popper.Modifiers = {
|
||||
flip: {
|
||||
enabled: false,
|
||||
},
|
||||
preventOverflow: {
|
||||
boundariesElement: 'window',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated We now provide a `Tooltip` Wildcard component that can be used directly instead of attaching
|
||||
* tooltips to an element using `data-tooltip`. This component will be removed after all use of `data-tooltip`
|
||||
* are migrated to use the new `Tooltip` component.
|
||||
*
|
||||
* Renders a Tooltip that can be positioned relative to a target element.
|
||||
*
|
||||
* This component should typically only need to be rendered once in a React tree.
|
||||
* If you need to attach a tooltip to an specific element, simply add the `data-tooltip` attribute to that element.
|
||||
*/
|
||||
export const Tooltip: React.FunctionComponent<React.PropsWithChildren<TooltipProps>> = ({ className }) => {
|
||||
const { subject, content, subjectSeq, placement = 'auto', delay } = useTooltipState()
|
||||
|
||||
const tooltipStyle = useMemo(() => getTooltipStyle(placement), [placement])
|
||||
|
||||
if (!subject || !content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapTooltip
|
||||
// Set key prop to work around a bug where quickly mousing between 2 elements with tooltips
|
||||
// displays the 2nd element's tooltip as still pointing to the first.
|
||||
key={subjectSeq}
|
||||
isOpen={true}
|
||||
target={subject}
|
||||
placement={placement}
|
||||
// in order to add our own placement classes we need to set the popperClassNames
|
||||
// here is where bootstrap injects it's placement classes such as 'bs-tooltip-auto' automatically.
|
||||
popperClassName={classNames(styles.tooltip, styles.show, className, tooltipStyle)}
|
||||
arrowClassName={styles.arrow}
|
||||
innerClassName={styles.tooltipInner}
|
||||
// This is a workaround to an issue with tooltips in reactstrap that causes the entire page to freeze.
|
||||
// Remove when https://github.com/reactstrap/reactstrap/issues/1482 is fixed.
|
||||
modifiers={TOOLTIP_MODIFIERS}
|
||||
delay={delay}
|
||||
>
|
||||
{content}
|
||||
</BootstrapTooltip>
|
||||
)
|
||||
}
|
||||
@ -2,6 +2,11 @@ import { UseTooltipReturn } from './useTooltipState'
|
||||
|
||||
type TooltipInstance = Pick<UseTooltipReturn, 'forceUpdate'>
|
||||
|
||||
/**
|
||||
* @deprecated We will be moving away from the `data-tooltip` pattern and now provide a `Tooltip` component
|
||||
* to use directly. This controller will be removed after all uses of `data-tooltip` are migrated to use the
|
||||
* new `Tooltip` component.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
export class TooltipController {
|
||||
private static instance: TooltipInstance | undefined
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './Tooltip'
|
||||
export * from './TooltipController'
|
||||
@ -22,7 +22,8 @@ export { Tabs, Tab, TabList, TabPanel, TabPanels, useTabsContext } from './Tabs'
|
||||
export { SourcegraphIcon } from './SourcegraphIcon'
|
||||
export { Badge, ProductStatusBadge, BADGE_VARIANTS, PRODUCT_STATUSES } from './Badge'
|
||||
export { Panel } from './Panel'
|
||||
export { Tooltip, TooltipController } from './Tooltip'
|
||||
export { Tooltip } from './Tooltip'
|
||||
export { Tooltip as DeprecatedTooltip, TooltipController as DeprecatedTooltipController } from './deprecated/Tooltip'
|
||||
export { Card, CardBody, CardHeader, CardList, CardSubtitle, CardText, CardTitle, CardFooter } from './Card'
|
||||
export { Icon } from './Icon'
|
||||
export { ButtonLink } from './ButtonLink'
|
||||
|
||||
@ -56,6 +56,7 @@ const config = {
|
||||
path.join(__dirname, 'client/shared/dev/mockPopper.ts'),
|
||||
path.join(__dirname, 'client/shared/dev/fetch'),
|
||||
path.join(__dirname, 'client/shared/dev/setLinkComponentForTest.ts'),
|
||||
path.join(__dirname, 'client/shared/dev/mockDomRect.ts'),
|
||||
path.join(__dirname, 'client/shared/dev/mockResizeObserver.ts'),
|
||||
path.join(__dirname, 'client/shared/dev/mockUniqueId.ts'),
|
||||
path.join(__dirname, 'client/shared/dev/mockSentryBrowser.ts'),
|
||||
|
||||
@ -359,6 +359,7 @@
|
||||
"@codemirror/view": "^0.20.4",
|
||||
"@lezer/highlight": "^0.16.0",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@radix-ui/react-tooltip": "^0.1.8 || 0.1.8-rc.2",
|
||||
"@reach/accordion": "^0.16.1",
|
||||
"@reach/combobox": "^0.16.5",
|
||||
"@reach/dialog": "^0.16.2",
|
||||
|
||||
208
yarn.lock
208
yarn.lock
@ -3469,6 +3469,200 @@
|
||||
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
|
||||
integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==
|
||||
|
||||
"@radix-ui/popper@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/popper/-/popper-0.1.0.tgz#c387a38f31b7799e1ea0d2bb1ca0c91c2931b063"
|
||||
integrity sha512-uzYeElL3w7SeNMuQpXiFlBhTT+JyaNMCwDfjKkrzugEcYrf5n52PHqncNdQPUtR42hJh8V9FsqyEDbDxkeNjJQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
csstype "^3.0.4"
|
||||
|
||||
"@radix-ui/primitive@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-0.1.0.tgz#6206b97d379994f0d1929809db035733b337e543"
|
||||
integrity sha512-tqxZKybwN5Fa3VzZry4G6mXAAb9aAqKmPtnVbZpL0vsBwvOHTBwsjHVPXylocYLwEtBY9SCe665bYnNB515uoA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-arrow@0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-0.1.4.tgz#a871448a418cd3507d83840fdd47558cb961672b"
|
||||
integrity sha512-BB6XzAb7Ml7+wwpFdYVtZpK1BlMgqyafSQNGzhIpSZ4uXvXOHPlR5GP8M449JkeQzgQjv9Mp1AsJxFC0KuOtuA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "0.1.4"
|
||||
|
||||
"@radix-ui/react-compose-refs@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-0.1.0.tgz#cff6e780a0f73778b976acff2c2a5b6551caab95"
|
||||
integrity sha512-eyclbh+b77k+69Dk72q3694OHrn9B3QsoIRx7ywX341U9RK1ThgQjMFZoPtmZNQTksXHLNEiefR8hGVeFyInGg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-context@0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-0.1.1.tgz#06996829ea124d9a1bc1dbe3e51f33588fab0875"
|
||||
integrity sha512-PkyVX1JsLBioeu0jB9WvRpDBBLtLZohVDT3BB5CTSJqActma8S8030P57mWZb4baZifMvN7KKWPAA40UmWKkQg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-dismissable-layer@0.1.5":
|
||||
version "0.1.5"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.1.5.tgz#9379032351e79028d472733a5cc8ba4a0ea43314"
|
||||
integrity sha512-J+fYWijkX4M4QKwf9dtu1oC0U6e6CEl8WhBp3Ad23yz2Hia0XCo6Pk/mp5CAFy4QBtQedTSkhW05AdtSOEoajQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "0.1.0"
|
||||
"@radix-ui/react-compose-refs" "0.1.0"
|
||||
"@radix-ui/react-primitive" "0.1.4"
|
||||
"@radix-ui/react-use-body-pointer-events" "0.1.1"
|
||||
"@radix-ui/react-use-callback-ref" "0.1.0"
|
||||
"@radix-ui/react-use-escape-keydown" "0.1.0"
|
||||
|
||||
"@radix-ui/react-id@0.1.5":
|
||||
version "0.1.5"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-0.1.5.tgz#010d311bedd5a2884c1e9bb6aaaa4e6cc1d1d3b8"
|
||||
integrity sha512-IPc4H/63bes0IZ1GJJozSEkSWcDyhNGtKFWUpJ+XtaLyQ1X3x7Mf6fWwWhDcpqlYEP+5WtAvfqcyEsyjP+ZhBQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-layout-effect" "0.1.0"
|
||||
|
||||
"@radix-ui/react-popper@0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-0.1.4.tgz#dfc055dcd7dfae6a2eff7a70d333141d15a5d029"
|
||||
integrity sha512-18gDYof97t8UQa7zwklG1Dr8jIdj3u+rVOQLzPi9f5i1YQak/pVGkaqw8aY+iDUknKKuZniTk/7jbAJUYlKyOw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/popper" "0.1.0"
|
||||
"@radix-ui/react-arrow" "0.1.4"
|
||||
"@radix-ui/react-compose-refs" "0.1.0"
|
||||
"@radix-ui/react-context" "0.1.1"
|
||||
"@radix-ui/react-primitive" "0.1.4"
|
||||
"@radix-ui/react-use-rect" "0.1.1"
|
||||
"@radix-ui/react-use-size" "0.1.1"
|
||||
"@radix-ui/rect" "0.1.1"
|
||||
|
||||
"@radix-ui/react-portal@0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-0.1.4.tgz#17bdce3d7f1a9a0b35cb5e935ab8bc562441a7d2"
|
||||
integrity sha512-MO0wRy2eYRTZ/CyOri9NANCAtAtq89DEtg90gicaTlkCfdqCLEBsLb+/q66BZQTr3xX/Vq01nnVfc/TkCqoqvw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "0.1.4"
|
||||
"@radix-ui/react-use-layout-effect" "0.1.0"
|
||||
|
||||
"@radix-ui/react-presence@0.1.2":
|
||||
version "0.1.2"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-0.1.2.tgz#9f11cce3df73cf65bc348e8b76d891f0d54c1fe3"
|
||||
integrity sha512-3BRlFZraooIUfRlyN+b/Xs5hq1lanOOo/+3h6Pwu2GMFjkGKKa4Rd51fcqGqnVlbr3jYg+WLuGyAV4KlgqwrQw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "0.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "0.1.0"
|
||||
|
||||
"@radix-ui/react-primitive@0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.4.tgz#6c233cf08b0cb87fecd107e9efecb3f21861edc1"
|
||||
integrity sha512-6gSl2IidySupIMJFjYnDIkIWRyQdbu/AHK7rbICPani+LW4b0XdxBXc46og/iZvuwW8pjCS8I2SadIerv84xYA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-slot" "0.1.2"
|
||||
|
||||
"@radix-ui/react-slot@0.1.2":
|
||||
version "0.1.2"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-0.1.2.tgz#e6f7ad9caa8ce81cc8d532c854c56f9b8b6307c8"
|
||||
integrity sha512-ADkqfL+agEzEguU3yS26jfB50hRrwf7U4VTwAOZEmi/g+ITcBWe12yM46ueS/UCIMI9Py+gFUaAdxgxafFvY2Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "0.1.0"
|
||||
|
||||
"@radix-ui/react-tooltip@^0.1.8 || 0.1.8-rc.2":
|
||||
version "0.1.8-rc.2"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-0.1.8-rc.2.tgz#f393141910b9cda541a41b928f1efc3626088d51"
|
||||
integrity sha512-2m5kGl/KiOHwehpskaE4KlJ/3c45iQJeZldjoOK71k1FfmGWIxd8Wy4yzMz+RfNNZXiGPYaMXSvXSmjMEVfrGA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "0.1.0"
|
||||
"@radix-ui/react-compose-refs" "0.1.0"
|
||||
"@radix-ui/react-context" "0.1.1"
|
||||
"@radix-ui/react-dismissable-layer" "0.1.5"
|
||||
"@radix-ui/react-id" "0.1.5"
|
||||
"@radix-ui/react-popper" "0.1.4"
|
||||
"@radix-ui/react-portal" "0.1.4"
|
||||
"@radix-ui/react-presence" "0.1.2"
|
||||
"@radix-ui/react-primitive" "0.1.4"
|
||||
"@radix-ui/react-slot" "0.1.2"
|
||||
"@radix-ui/react-use-controllable-state" "0.1.0"
|
||||
"@radix-ui/react-visually-hidden" "0.1.4"
|
||||
|
||||
"@radix-ui/react-use-body-pointer-events@0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.1.1.tgz#63e7fd81ca7ffd30841deb584cd2b7f460df2597"
|
||||
integrity sha512-R8leV2AWmJokTmERM8cMXFHWSiv/fzOLhG/JLmRBhLTAzOj37EQizssq4oW0Z29VcZy2tODMi9Pk/htxwb+xpA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-layout-effect" "0.1.0"
|
||||
|
||||
"@radix-ui/react-use-callback-ref@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-0.1.0.tgz#934b6e123330f5b3a6b116460e6662cbc663493f"
|
||||
integrity sha512-Va041McOFFl+aV+sejvl0BS2aeHx86ND9X/rVFmEFQKTXCp6xgUK0NGUAGcgBlIjnJSbMYPGEk1xKSSlVcN2Aw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-controllable-state@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-0.1.0.tgz#4fced164acfc69a4e34fb9d193afdab973a55de1"
|
||||
integrity sha512-zv7CX/PgsRl46a52Tl45TwqwVJdmqnlQEQhaYMz/yBOD2sx2gCkCFSoF/z9mpnYWmS6DTLNTg5lIps3fV6EnXg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-callback-ref" "0.1.0"
|
||||
|
||||
"@radix-ui/react-use-escape-keydown@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-0.1.0.tgz#dc80cb3753e9d1bd992adbad9a149fb6ea941874"
|
||||
integrity sha512-tDLZbTGFmvXaazUXXv8kYbiCcbAE8yKgng9s95d8fCO+Eundv0Jngbn/hKPhDDs4jj9ChwRX5cDDnlaN+ugYYQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-callback-ref" "0.1.0"
|
||||
|
||||
"@radix-ui/react-use-layout-effect@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-0.1.0.tgz#ebf71bd6d2825de8f1fbb984abf2293823f0f223"
|
||||
integrity sha512-+wdeS51Y+E1q1Wmd+1xSSbesZkpVj4jsg0BojCbopWvgq5iBvixw5vgemscdh58ep98BwUbsFYnrywFhV9yrVg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-rect@0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-0.1.1.tgz#6c15384beee59c086e75b89a7e66f3d2e583a856"
|
||||
integrity sha512-kHNNXAsP3/PeszEmM/nxBBS9Jbo93sO+xuMTcRfwzXsmxT5gDXQzAiKbZQ0EecCPtJIzqvr7dlaQi/aP1PKYqQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/rect" "0.1.1"
|
||||
|
||||
"@radix-ui/react-use-size@0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-0.1.1.tgz#f6b75272a5d41c3089ca78c8a2e48e5f204ef90f"
|
||||
integrity sha512-pTgWM5qKBu6C7kfKxrKPoBI2zZYZmp2cSXzpUiGM3qEBQlMLtYhaY2JXdXUCxz+XmD1YEjc8oRwvyfsD4AG4WA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-visually-hidden@0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-0.1.4.tgz#6c75eae34fb5d084b503506fbfc05587ced05f03"
|
||||
integrity sha512-K/q6AEEzqeeEq/T0NPChvBqnwlp8Tl4NnQdrI/y8IOY7BRR+Ug0PEsVk6g48HJ7cA1//COugdxXXVVK/m0X1mA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "0.1.4"
|
||||
|
||||
"@radix-ui/rect@0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/@radix-ui/rect/-/rect-0.1.1.tgz#95b5ba51f469bea6b1b841e2d427e17e37d38419"
|
||||
integrity sha512-g3hnE/UcOg7REdewduRPAK88EPuLZtaq7sA9ouu8S+YEtnyFRI16jgv6GZYe3VMoQLL1T171ebmEPtDjyxWLzw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@reach/accordion@^0.16.1":
|
||||
version "0.16.1"
|
||||
resolved "https://registry.npmjs.org/@reach/accordion/-/accordion-0.16.1.tgz#020fcbfa0d0768a95ca1702952b87eafbfbc7f38"
|
||||
@ -10360,10 +10554,10 @@ csstype@^2.5.7:
|
||||
resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.4.tgz#d585a6062096e324e7187f80e04f92bd0f00e37f"
|
||||
integrity sha512-lAJUJP3M6HxFXbqtGRc0iZrdyeN+WzOWeY0q/VnFzI+kqVrYIzC7bWlKqCW7oCIdzoPkvfp82EVvrTlQ8zsWQg==
|
||||
|
||||
csstype@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.npmjs.org/csstype/-/csstype-3.0.2.tgz#ee5ff8f208c8cd613b389f7b222c9801ca62b3f7"
|
||||
integrity sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==
|
||||
csstype@^3.0.2, csstype@^3.0.4:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
||||
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
|
||||
|
||||
currently-unhandled@^0.4.1:
|
||||
version "0.4.1"
|
||||
@ -21607,9 +21801,9 @@ regenerate@^1.4.0:
|
||||
integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
|
||||
|
||||
regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7:
|
||||
version "0.13.7"
|
||||
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
||||
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
|
||||
version "0.13.9"
|
||||
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
|
||||
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
|
||||
|
||||
regenerator-transform@^0.14.2:
|
||||
version "0.14.2"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user