web: upgrade Storybook (#36437)

This commit is contained in:
Valery Bugakov 2022-06-06 20:30:11 -07:00 committed by GitHub
parent 3fefbdf487
commit 4a09ea0f27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1114 additions and 1315 deletions

View File

@ -70,6 +70,10 @@ const config = {
message:
'Please use components from the Wildcard component library instead. We work on removing `reactstrap` dependency.',
},
{
name: 'chromatic/isChromatic',
message: 'Please use `isChromatic` from the `@sourcegraph/storybook` package.',
},
],
patterns: [
{

View File

@ -3,3 +3,5 @@ import path from 'path'
export const ROOT_PATH = path.resolve(__dirname, '../../../')
export const NODE_MODULES_PATH = path.resolve(ROOT_PATH, 'node_modules')
export const MONACO_EDITOR_PATH = path.resolve(NODE_MODULES_PATH, 'monaco-editor')
export const STATIC_ASSETS_PATH = path.join(ROOT_PATH, 'ui/assets')
export const STATIC_INDEX_PATH = path.resolve(STATIC_ASSETS_PATH, 'index.html')

View File

@ -13,6 +13,7 @@ import * as prettier from 'prettier'
import { Subject, Subscription, throwError } from 'rxjs'
import { first, timeoutWith } from 'rxjs/operators'
import { STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
import { asError, keyExistsIn } from '@sourcegraph/common'
import { ErrorGraphQLResult, SuccessGraphQLResult } from '@sourcegraph/http-client'
// eslint-disable-next-line no-restricted-imports
@ -31,8 +32,6 @@ util.inspect.defaultOptions.maxStringLength = 80
Polly.register(CdpAdapter as any)
Polly.register(FSPersister)
const ASSETS_DIRECTORY = path.resolve(__dirname, '../../../../../ui/assets')
const checkPollyMode = (mode: string): MODE => {
if (mode === 'record' || mode === 'replay' || mode === 'passthrough' || mode === 'stopped') {
return mode
@ -179,7 +178,7 @@ export const createSharedIntegrationTestContext = async <
// Cache all responses for the entire lifetime of the test run
response.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
try {
const content = await readFile(path.join(ASSETS_DIRECTORY, asset), {
const content = await readFile(path.join(STATIC_ASSETS_PATH, asset), {
// Polly doesn't support Buffers or streams at the moment
encoding: 'utf-8',
})

View File

@ -8,11 +8,12 @@
"main": "./src/index.ts",
"scripts": {
"lint:js": "eslint --cache 'src/**/*.[jt]s?(x)'",
"start": "TS_NODE_TRANSPILE_ONLY=true start-storybook -p 9001 -c ./src -s ./assets,../../ui/assets",
"build": "TS_NODE_TRANSPILE_ONLY=true build-storybook -c ./src -s ./assets,../../ui/assets",
"build:webpack-stats": "TS_NODE_TRANSPILE_ONLY=true WEBPACK_DLL_PLUGIN=false start-storybook -c ./src -s ./assets --smoke-test --webpack-stats-json ./storybook-static --loglevel warn",
"start": "TS_NODE_TRANSPILE_ONLY=true start-storybook -p 9001 -c ./src",
"start:chromatic": "CHROMATIC=true TS_NODE_TRANSPILE_ONLY=true start-storybook -p 9001 -c ./src",
"build": "TS_NODE_TRANSPILE_ONLY=true build-storybook -c ./src",
"build:webpack-stats": "TS_NODE_TRANSPILE_ONLY=true WEBPACK_DLL_PLUGIN=false start-storybook -c ./src --smoke-test --webpack-stats-json ./storybook-static --loglevel warn",
"build:dll-bundle": "TS_NODE_TRANSPILE_ONLY=true webpack --config ./src/webpack.config.dll.ts --no-stats",
"start:dll": "TS_NODE_TRANSPILE_ONLY=true WEBPACK_DLL_PLUGIN=true start-storybook -p 9001 -c ./src -s ./assets",
"start:dll": "TS_NODE_TRANSPILE_ONLY=true WEBPACK_DLL_PLUGIN=true start-storybook -p 9001 -c ./src",
"clean:dll": "rm -rf assets/dll-bundle storybook-static/*-stats.json",
"test": "jest"
}

View File

@ -1,30 +0,0 @@
import { PublishedStoreItem } from '@storybook/client-api'
import { raw } from '@storybook/react'
import isChromatic from 'chromatic/isChromatic'
import { addStory } from './add-story'
import { storyStore } from './story-store'
// Execute logic below only in the environment where Chromatic snapshots are captured.
if (isChromatic()) {
// CSF stories need to be evaluated before they are added to the `StoryStore` and thus are not immediately available.
// We setTimeout to delay this logic until all stories have been added.
setTimeout(() => {
// Get an array of all stories which are already added to the `StoryStore`.
// Use `raw()` because we don't want to apply any filtering and sorting on the array of stories.
const storeItems = raw() as PublishedStoreItem[]
// `StoryStore` is immutable outside of a configure() call.
// As we delay this logic to support CSF stories, we need to set this to ensure changes are still applied.
storyStore.startConfiguring()
// Add three more versions of each story to test visual regressions with Chromatic snapshots.
// In other environments, these themes can be explored by a user via toolbar toggles.
for (const storeItem of storeItems) {
// Default theme + Dark mode.
addStory({ storeItem })
}
storyStore.finishConfiguring()
}, 0)
}

View File

@ -1,46 +0,0 @@
import { PublishedStoreItem } from '@storybook/client-api'
import { toId } from '@storybook/csf'
import { createChromaticStory } from './create-chromatic-story'
import { storyStore } from './story-store'
interface AddStoryOptions {
storeItem: PublishedStoreItem
}
export const addStory = (options: AddStoryOptions): void => {
const {
storeItem: { name, kind, storyFn, parameters },
} = options
const isDarkModeEnabled = Boolean(parameters?.chromatic?.enableDarkMode)
// Add suffix to the story name based on theme options:
// 1. Default + Dark: "Text" -> "Text 🌚"
const storyName = [name, isDarkModeEnabled && '🌚'].filter(Boolean).join(' ')
/**
* Use `storyStore.addStory()` to avoid applying decorators to stories, because `PublishedStoreItem.storyFn` already has decorators applied.
* `storiesOf().add()` usage API would result in decorators duplication. It's possible to avoid this issue using `PublishedStoreItem.getOriginal()`,
* which returns only story function without any decorators and story context. It means that we should apply them manually and
* keep this logic in sync with Storybook internals to have consistent behavior. `storyStore.addStory()` allows to avoid it.
*/
storyStore.addStory(
{
id: toId(kind, storyName),
kind,
name: storyName,
parameters,
loaders: [],
storyFn: createChromaticStory({
storyFn,
isDarkModeEnabled,
}),
},
{
// The default `applyDecorators` implementation accepts `decorators` as a second arg and applies them to the `storyFn`.
// Our `storyFn` already has all the decorators applied, so we just return it.
applyDecorators: storyFunc => storyFunc,
}
)
}

View File

@ -1,41 +0,0 @@
import React, { ReactElement, useEffect } from 'react'
import { StoryFn } from '@storybook/addons'
import { useDarkMode } from 'storybook-dark-mode'
import { THEME_DARK_CLASS, THEME_LIGHT_CLASS } from '../themes'
export interface CreateChromaticStoryOptions {
storyFn: StoryFn<ReactElement>
isDarkModeEnabled: boolean
}
// Wrap `storyFn` into a decorator which takes care of CSS classes toggling based on received theme options.
export const createChromaticStory = (options: CreateChromaticStoryOptions): StoryFn => () => {
const { storyFn, isDarkModeEnabled } = options
// The `storyFn` is retrieved from the `StoryStore`, so it already has a `StoryContext`.
// We can safely change its type to remove required props `StoryContext` props check.
const Story = storyFn as React.ComponentType<React.PropsWithChildren<unknown>>
const isDarkModeEnabledInitially = useDarkMode()
useEffect(() => {
// 'storybook-dark-mode' doesn't expose any API to toggle dark/light theme programmatically, so we do it manually.
document.body.classList.toggle(THEME_DARK_CLASS, isDarkModeEnabled)
document.body.classList.toggle(THEME_LIGHT_CLASS, !isDarkModeEnabled)
document.body.dispatchEvent(new CustomEvent('chromatic-light-theme-toggled', { detail: !isDarkModeEnabled }))
return () => {
// Always toggle dark mode back to the previous value because otherwise, it might be out of sync with the toolbar toggle.
document.body.classList.toggle(THEME_DARK_CLASS, isDarkModeEnabledInitially)
document.body.classList.toggle(THEME_LIGHT_CLASS, !isDarkModeEnabledInitially)
document.body.dispatchEvent(
new CustomEvent('chromatic-light-theme-toggled', { detail: !isDarkModeEnabledInitially })
)
}
// We need to execute `useEffect` callback once to take snapshot in Chromatic, so we can omit dependencies here.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return <Story />
}

View File

@ -1,13 +0,0 @@
import { StoryStore } from '@storybook/client-api'
// This global reference is used internally by Storybook:
// https://github.com/storybookjs/storybook/blob/3ec358f71c6111838092397d13fbe35b627a9a9d/lib/core-client/src/preview/start.ts#L43
declare global {
interface Window {
__STORYBOOK_STORY_STORE__: StoryStore
}
}
// See the discussion about `StoryStore` usage in stories:
// https://github.com/storybookjs/storybook/discussions/12050#discussioncomment-125658
export const storyStore = window.__STORYBOOK_STORY_STORE__

View File

@ -0,0 +1,7 @@
.theme-wrapper {
padding: 1rem;
color: var(--body-color);
background-color: var(--body-bg);
position: relative;
min-height: 50vh;
}

View File

@ -0,0 +1,31 @@
import { FunctionComponent, PropsWithChildren, useState } from 'react'
import classNames from 'classnames'
import { PopoverRoot } from '@sourcegraph/wildcard'
import { ChromaticThemeContext, ChromaticTheme } from '../../../hooks/useChromaticTheme'
import styles from './ChromaticRoot.module.scss'
interface ChromaticRootProps extends ChromaticTheme {}
export const ChromaticRoot: FunctionComponent<PropsWithChildren<ChromaticRootProps>> = props => {
const { theme, children } = props
const [rootReference, setElement] = useState<HTMLDivElement | null>(null)
const themeClass = theme === 'light' ? 'theme-light' : 'theme-dark'
return (
<ChromaticThemeContext.Provider value={{ theme }}>
{/* Required to render `Popover` inside of the `ChromaticRoot` component. */}
<PopoverRoot.Provider value={{ renderRoot: rootReference }}>
<div className={classNames(themeClass, styles.themeWrapper)}>
{children}
<div ref={setElement} />
</div>
</PopoverRoot.Provider>
</ChromaticThemeContext.Provider>
)
}

View File

@ -0,0 +1 @@
export * from './ChromaticRoot'

View File

@ -0,0 +1 @@
export * from './withChromaticThemes'

View File

@ -0,0 +1,32 @@
import { ReactElement } from 'react'
import { DecoratorFunction } from '@storybook/addons'
import { ChromaticRoot } from './ChromaticRoot'
/**
* The global Storybook decorator used to snapshot stories with multiple themes in Chromatic.
*
* It's a recommended way of achieving this goal:
* https://www.chromatic.com/docs/faq#do-you-support-taking-snapshots-of-a-component-with-multiple-the
*
* If the `chromatic.enableDarkMode` story parameter is set to `true`, the story will
* be rendered twice in Chromatic in light and dark modes.
*/
export const withChromaticThemes: DecoratorFunction<ReactElement> = (StoryFunc, { parameters }) => {
if (parameters?.chromatic?.enableDarkMode) {
return (
<>
<ChromaticRoot theme="light">
<StoryFunc />
</ChromaticRoot>
<ChromaticRoot theme="dark">
<StoryFunc />
</ChromaticRoot>
</>
)
}
return <StoryFunc />
}

View File

@ -7,4 +7,5 @@ export const ENVIRONMENT_CONFIG = {
WEBPACK_BUNDLE_ANALYZER: getEnvironmentBoolean('WEBPACK_BUNDLE_ANALYZER'),
WEBPACK_SPEED_ANALYZER: getEnvironmentBoolean('WEBPACK_SPEED_ANALYZER'),
MINIFY: getEnvironmentBoolean('MINIFY'),
CHROMATIC: getEnvironmentBoolean('CHROMATIC'),
}

View File

@ -0,0 +1,13 @@
import { createContext, useContext } from 'react'
export interface ChromaticTheme {
theme: 'light' | 'dark'
}
export const ChromaticThemeContext = createContext<ChromaticTheme>({
theme: 'light',
})
export function useChromaticDarkMode(): boolean {
return useContext(ChromaticThemeContext).theme === 'dark'
}

View File

@ -1,6 +1,12 @@
import { useLayoutEffect, useState } from 'react'
import { useDarkMode } from 'storybook-dark-mode'
import { useDarkMode as useRegularDarkMode } from 'storybook-dark-mode'
import { isChromatic } from '../utils/isChromatic'
import { useChromaticDarkMode } from './useChromaticTheme'
const useDarkMode = isChromatic() ? useChromaticDarkMode : useRegularDarkMode
/**
* Gets current theme and updates value when theme changes
@ -17,16 +23,5 @@ export const useTheme = (): boolean => {
setIsLightTheme(!isDarkMode)
}, [isDarkMode])
// This is required for Chromatic to react to theme changes when
// taking screenshots. See `create-chromatic-story.tsx` where
// this event is dispatched.
useLayoutEffect(() => {
const listener = ((event: CustomEvent<boolean>): void => {
setIsLightTheme(event.detail)
}) as EventListener
document.body.addEventListener('chromatic-light-theme-toggled', listener)
return () => document.body.removeEventListener('chromatic-light-theme-toggled', listener)
}, [])
return isLightTheme
}

View File

@ -1,3 +1,4 @@
export * from './apollo/MockedStoryProvider'
export * from './hooks/usePrependStyles'
export * from './hooks/useTheme'
export * from './utils/isChromatic'

View File

@ -21,6 +21,7 @@ import {
getBabelLoader,
getBasicCSSLoader,
getStatoscopePlugin,
STATIC_ASSETS_PATH,
} from '@sourcegraph/build-config'
import { ensureDllBundleIsReady } from './dllPlugin'
@ -38,9 +39,6 @@ const getStoriesGlob = (): string[] => {
return [path.resolve(ROOT_PATH, ENVIRONMENT_CONFIG.STORIES_GLOB)]
}
// Stories in `Chromatic.story.tsx` are guarded by the `isChromatic()` check. It will result in noop in all other environments.
const chromaticStoriesGlob = path.resolve(ROOT_PATH, 'client/storybook/src/chromatic-story/Chromatic.story.tsx')
// Due to an issue with constant recompiling (https://github.com/storybookjs/storybook/issues/14342)
// we need to make the globs more specific (`(web|shared..)` also doesn't work). Once the above issue
// is fixed, this can be removed and watched for `client/**/*.story.tsx` again.
@ -49,7 +47,7 @@ const getStoriesGlob = (): string[] => {
path.resolve(ROOT_PATH, `client/${packageDirectory}/src/**/*.story.tsx`)
)
return [...storiesGlobs, chromaticStoriesGlob]
return [...storiesGlobs]
}
const getDllScriptTag = (): string => {
@ -65,6 +63,7 @@ const getDllScriptTag = (): string => {
}
const config = {
staticDirs: [path.resolve(__dirname, '../assets'), STATIC_ASSETS_PATH],
stories: getStoriesGlob(),
addons: [
'@storybook/addon-knobs',
@ -77,6 +76,9 @@ const config = {
core: {
builder: 'webpack5',
options: {
fsCache: true,
},
},
features: {
@ -111,6 +113,7 @@ const config = {
new DefinePlugin({
NODE_ENV: JSON.stringify(config.mode),
'process.env.NODE_ENV': JSON.stringify(config.mode),
'process.env.CHROMATIC': JSON.stringify(ENVIRONMENT_CONFIG.CHROMATIC),
}),
getProvidePlugin()
)

View File

@ -3,20 +3,22 @@ import { ReactElement } from 'react'
import { configureActions } from '@storybook/addon-actions'
import { withConsole } from '@storybook/addon-console'
import { DecoratorFunction } from '@storybook/addons'
import isChromatic from 'chromatic/isChromatic'
import { DecoratorFunction, Parameters } from '@storybook/addons'
import { withDesign } from 'storybook-addon-designs'
import { setLinkComponent, AnchorLink } from '@sourcegraph/wildcard'
import { withChromaticThemes } from './decorators/withChromaticThemes'
import { themeDark, themeLight, THEME_DARK_CLASS, THEME_LIGHT_CLASS } from './themes'
import { isChromatic } from './utils/isChromatic'
const withConsoleDecorator: DecoratorFunction<ReactElement> = (storyFunc, context): ReactElement =>
withConsole()(storyFunc)(context)
export const decorators = [withDesign, withConsoleDecorator]
export const decorators = [withDesign, withConsoleDecorator, isChromatic() && withChromaticThemes].filter(Boolean)
export const parameters = {
export const parameters: Parameters = {
layout: 'fullscreen',
options: {
storySort: {
order: ['wildcard', 'shared', 'branded', '*'],

View File

@ -0,0 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import isChromaticDefault from 'chromatic/isChromatic'
/**
* `chromatic/isChromatic` wrapper that takes into account `process.env.CHROMATIC` for local testing.
*/
export const isChromatic = (): boolean => isChromaticDefault() || Boolean(process.env.CHROMATIC)

View File

@ -3,9 +3,9 @@ import path from 'path'
import * as esbuild from 'esbuild'
import signale from 'signale'
import { MONACO_LANGUAGES_AND_FEATURES } from '@sourcegraph/build-config'
import { MONACO_LANGUAGES_AND_FEATURES, ROOT_PATH, STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
import { ENVIRONMENT_CONFIG, ROOT_PATH, STATIC_ASSETS_PATH } from '../utils'
import { ENVIRONMENT_CONFIG } from '../utils'
import { manifestPlugin } from './manifestPlugin'
import { monacoPlugin } from './monacoPlugin'

View File

@ -3,7 +3,8 @@ import path from 'path'
import * as esbuild from 'esbuild'
import { STATIC_ASSETS_PATH } from '../utils'
import { STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
import { WebpackManifest } from '../webpack/get-html-webpack-plugins'
export const assetPathPrefix = '/.assets'

View File

@ -4,9 +4,7 @@ import * as esbuild from 'esbuild'
import { EditorFeature, featuresArr } from 'monaco-editor-webpack-plugin/out/features'
import { EditorLanguage, languagesArr } from 'monaco-editor-webpack-plugin/out/languages'
import { MONACO_LANGUAGES_AND_FEATURES } from '@sourcegraph/build-config'
import { ROOT_PATH } from '../utils'
import { MONACO_LANGUAGES_AND_FEATURES, ROOT_PATH } from '@sourcegraph/build-config'
const monacoModulePath = (modulePath: string): string =>
require.resolve(path.join('monaco-editor/esm', modulePath), {

View File

@ -5,7 +5,7 @@ import express from 'express'
import { createProxyMiddleware } from 'http-proxy-middleware'
import signale from 'signale'
import { STATIC_ASSETS_PATH } from '../utils'
import { STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
import { buildMonaco, BUILD_OPTIONS } from './build'
import { assetPathPrefix } from './manifestPlugin'

View File

@ -6,10 +6,11 @@ import postcss from 'postcss'
import postcssModules from 'postcss-modules'
import sass from 'sass'
import { ROOT_PATH } from '@sourcegraph/build-config'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import postcssConfig from '../../../../postcss.config'
import { ROOT_PATH } from '../utils'
/**
* An esbuild plugin that builds .css and .scss stylesheets (including support for CSS modules).

View File

@ -5,13 +5,14 @@ import signale from 'signale'
import createWebpackCompiler, { Configuration } from 'webpack'
import WebpackDevServer, { ProxyConfigArrayItem } from 'webpack-dev-server'
import { STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
import { getManifest } from '../esbuild/manifestPlugin'
import { esbuildDevelopmentServer } from '../esbuild/server'
import {
ENVIRONMENT_CONFIG,
getAPIProxySettings,
shouldCompressResponse,
STATIC_ASSETS_PATH,
STATIC_ASSETS_URL,
HTTPS_WEB_SERVER_URL,
HTTP_WEB_SERVER_URL,

View File

@ -5,12 +5,12 @@ import expressStaticGzip from 'express-static-gzip'
import { createProxyMiddleware } from 'http-proxy-middleware'
import signale from 'signale'
import { STATIC_ASSETS_PATH, STATIC_INDEX_PATH } from '@sourcegraph/build-config'
import {
PROXY_ROUTES,
getAPIProxySettings,
ENVIRONMENT_CONFIG,
STATIC_ASSETS_PATH,
STATIC_INDEX_PATH,
HTTP_WEB_SERVER_URL,
HTTPS_WEB_SERVER_URL,
} from '../utils'

View File

@ -1,8 +1,7 @@
import path from 'path'
export const ROOT_PATH = path.resolve(__dirname, '../../../../')
export const STATIC_ASSETS_PATH = path.resolve(ROOT_PATH, 'ui/assets')
export const STATIC_INDEX_PATH = path.resolve(STATIC_ASSETS_PATH, 'index.html')
import { ROOT_PATH } from '@sourcegraph/build-config'
export const STATIC_ASSETS_URL = '/.assets/'
export const DEV_SERVER_LISTEN_ADDR = { host: 'localhost', port: 3080 } as const
export const DEV_SERVER_PROXY_TARGET_ADDR = { host: 'localhost', port: 3081 } as const

View File

@ -5,7 +5,9 @@ import HtmlWebpackPlugin, { TemplateParameter, Options } from 'html-webpack-plug
import signale from 'signale'
import { WebpackPluginInstance } from 'webpack'
import { createJsContext, ENVIRONMENT_CONFIG, STATIC_ASSETS_PATH } from '../utils'
import { STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
import { createJsContext, ENVIRONMENT_CONFIG } from '../utils'
const { SOURCEGRAPH_HTTPS_PORT, NODE_ENV } = ENVIRONMENT_CONFIG

View File

@ -0,0 +1,4 @@
.container {
padding: 2rem;
min-height: 60vh;
}

View File

@ -22,6 +22,8 @@ import {
FIXTURE_WARNING_MARKDOWN_ALERT,
} from './WebHoverOverlay.fixtures'
import styles from './WebHoverOverlay.story.module.scss'
registerHighlightContributions()
const { add } = storiesOf('web/WebHoverOverlay', module)
@ -210,14 +212,16 @@ add('With long markdown text and dismissible alert with icon.', () => (
))
add('Multiple MarkupContents with badges and alerts', () => (
<WebHoverOverlay
{...commonProps()}
hoverOrError={{
contents: [FIXTURE_CONTENT, FIXTURE_CONTENT, FIXTURE_CONTENT],
aggregatedBadges: [FIXTURE_SEMANTIC_BADGE],
alerts: [FIXTURE_SMALL_TEXT_MARKDOWN_ALERT, FIXTURE_WARNING_MARKDOWN_ALERT],
}}
actionsOrError={FIXTURE_ACTIONS}
onAlertDismissed={action('onAlertDismissed')}
/>
<div className={styles.container}>
<WebHoverOverlay
{...commonProps()}
hoverOrError={{
contents: [FIXTURE_CONTENT, FIXTURE_CONTENT, FIXTURE_CONTENT],
aggregatedBadges: [FIXTURE_SEMANTIC_BADGE],
alerts: [FIXTURE_SMALL_TEXT_MARKDOWN_ALERT, FIXTURE_WARNING_MARKDOWN_ALERT],
}}
actionsOrError={FIXTURE_ACTIONS}
onAlertDismissed={action('onAlertDismissed')}
/>
</div>
))

View File

@ -1,9 +1,10 @@
import { boolean } from '@storybook/addon-knobs'
import { storiesOf } from '@storybook/react'
import isChromatic from 'chromatic/isChromatic'
import classNames from 'classnames'
import { subDays } from 'date-fns'
import { isChromatic } from '@sourcegraph/storybook'
import { WebStory } from '../../../components/WebStory'
import { BatchChangeNode } from './BatchChangeNode'

View File

@ -1,4 +1,5 @@
import isChromatic from 'chromatic/isChromatic'
import { isChromatic } from '@sourcegraph/storybook'
const incrementedLocalStorageKeys = new Map<string, number>()
/**

View File

@ -29,7 +29,6 @@ export const Default: Story = () => (
Default.parameters = {
component: Modal,
chromatic: {
enableDarkMode: true,
disableSnapshot: false,
},
design: [

View File

@ -2,7 +2,7 @@ import { FunctionComponent, MutableRefObject, PropsWithChildren, useCallback, us
import { noop } from 'lodash'
import { PopoverContext } from './context'
import { PopoverContext } from './contexts/internal-context'
export enum PopoverOpenEventReason {
TriggerClick = 'TriggerClick',

View File

@ -4,7 +4,7 @@ import { noop } from 'lodash'
import { useCallbackRef, useMergeRefs } from 'use-callback-ref'
import { ForwardReferenceComponent } from '../../../types'
import { PopoverContext } from '../context'
import { PopoverContext } from '../contexts/internal-context'
import { PopoverOpenEventReason } from '../Popover'
interface PopoverTriggerProps {}

View File

@ -15,6 +15,13 @@ export interface FloatingPanelProps extends Omit<Tether, 'target' | 'element'>,
* Renders nothing if target isn't specified.
*/
target: HTMLElement | null
/**
* The root element where Popover renders popover content element.
* This element is used when we render popover with fixed strategy -
* outside the dom tree.
*/
rootRender?: HTMLElement | null
}
/**
@ -37,6 +44,7 @@ export const FloatingPanel = forwardRef((props, reference) => {
constraintPadding,
targetPadding,
constraint,
rootRender,
...otherProps
} = props
@ -100,6 +108,6 @@ export const FloatingPanel = forwardRef((props, reference) => {
<Component {...otherProps} ref={references} className={classNames(styles.floatingPanel, otherProps.className)}>
{props.children}
</Component>,
document.body
rootRender ?? document.body
)
}) as ForwardReferenceComponent<'div', PropsWithChildren<FloatingPanelProps>>

View File

@ -6,7 +6,8 @@ import { useCallbackRef, useMergeRefs } from 'use-callback-ref'
import { useKeyboard, useOnClickOutside } from '../../../../hooks'
import { ForwardReferenceComponent } from '../../../../types'
import { PopoverContext } from '../../context'
import { PopoverContext } from '../../contexts/internal-context'
import { PopoverRoot } from '../../contexts/public-context'
import { PopoverOpenEventReason } from '../../Popover'
import { FloatingPanel, FloatingPanelProps } from '../floating-panel/FloatingPanel'
@ -31,6 +32,8 @@ export const PopoverContent = forwardRef((props, reference) => {
} = props
const { isOpen: isOpenContext, targetElement, tailElement, anchor, setOpen } = useContext(PopoverContext)
const { renderRoot } = useContext(PopoverRoot)
const [focusLock, setFocusLock] = useState(false)
const [tooltipElement, setTooltipElement] = useState<HTMLDivElement | null>(null)
@ -78,6 +81,7 @@ export const PopoverContent = forwardRef((props, reference) => {
marker={tailElement}
role={role}
aria-modal={ariaModel}
rootRender={renderRoot}
className={classNames(styles.popover, otherProps.className)}
>
{focusLocked ? (

View File

@ -2,7 +2,8 @@ import { FunctionComponent, HTMLAttributes, useContext } from 'react'
import { createPortal } from 'react-dom'
import { PopoverContext } from '../../context'
import { PopoverContext } from '../../contexts/internal-context'
import { PopoverRoot } from '../../contexts/public-context'
import style from './PopoverTail.module.scss'
@ -10,6 +11,7 @@ interface PopoverTailProps extends HTMLAttributes<SVGElement> {}
export const PopoverTail: FunctionComponent<PopoverTailProps> = props => {
const { setTailElement, isOpen } = useContext(PopoverContext)
const { renderRoot } = useContext(PopoverRoot)
if (!isOpen) {
return null
@ -19,6 +21,6 @@ export const PopoverTail: FunctionComponent<PopoverTailProps> = props => {
<svg {...props} width="17.2" height="11" viewBox="0 0 200 130" className={style.tail} ref={setTailElement}>
<path d="M0,0 L100,130 200,0" className={style.tailTrianglePath} />
</svg>,
document.body
renderRoot ?? document.body
)
}

View File

@ -2,9 +2,9 @@ import { createContext, MutableRefObject } from 'react'
import { noop } from 'lodash'
import { PopoverOpenEvent } from './Popover'
import { PopoverOpenEvent } from '../Popover'
export interface PopoverContextData {
export interface PopoverInternalContextData {
isOpen: boolean
targetElement: HTMLElement | null
tailElement: SVGGElement | null
@ -14,7 +14,7 @@ export interface PopoverContextData {
setTailElement: (element: SVGGElement | null) => void
}
const DEFAULT_CONTEXT_VALUE: PopoverContextData = {
const DEFAULT_CONTEXT_VALUE: PopoverInternalContextData = {
isOpen: false,
targetElement: null,
tailElement: null,
@ -23,4 +23,4 @@ const DEFAULT_CONTEXT_VALUE: PopoverContextData = {
setTailElement: noop,
}
export const PopoverContext = createContext<PopoverContextData>(DEFAULT_CONTEXT_VALUE)
export const PopoverContext = createContext<PopoverInternalContextData>(DEFAULT_CONTEXT_VALUE)

View File

@ -0,0 +1,11 @@
import { createContext } from 'react'
interface PopoverRootData {
renderRoot: HTMLElement | null
}
const DEFAULT_POPOVER_PROVIDER_INFO: PopoverRootData = {
renderRoot: null,
}
export const PopoverRoot = createContext<PopoverRootData>(DEFAULT_POPOVER_PROVIDER_INFO)

View File

@ -3,6 +3,7 @@ export * from './Popover'
export * from './components/PopoverTrigger'
export * from './components/popover-content'
export * from './components/popover-tail'
export * from './contexts/public-context'
// Tether (popover, tooltip) position calculation engine
export * from './tether'

View File

@ -162,7 +162,6 @@ export const PositionSettingsGallery: Story = () => {
PositionSettingsGallery.parameters = {
chromatic: {
enableDarkMode: true,
disableSnapshot: false,
},
}

View File

@ -39,6 +39,7 @@ export {
PopoverContent,
Position,
PopoverTail,
PopoverRoot,
PopoverOpenEventReason,
EMPTY_RECTANGLE,
createRectangle,

View File

@ -138,24 +138,24 @@
"@sourcegraph/stylelint-plugin-sourcegraph": "^1.0.1",
"@sourcegraph/tsconfig": "^4.0.1",
"@statoscope/webpack-plugin": "^5.20.1",
"@storybook/addon-a11y": "^6.3.12",
"@storybook/addon-actions": "^6.3.12",
"@storybook/addon-a11y": "^6.5.7",
"@storybook/addon-actions": "^6.5.7",
"@storybook/addon-console": "^1.2.3",
"@storybook/addon-knobs": "^6.3.1",
"@storybook/addon-links": "^6.3.12",
"@storybook/addon-storyshots": "^6.3.12",
"@storybook/addon-storyshots-puppeteer": "^6.3.12",
"@storybook/addon-toolbars": "^6.3.12",
"@storybook/addons": "^6.3.12",
"@storybook/api": "^6.3.12",
"@storybook/builder-webpack5": "^6.3.12",
"@storybook/client-api": "^6.3.12",
"@storybook/components": "^6.3.12",
"@storybook/core": "^6.3.12",
"@storybook/core-events": "^6.3.12",
"@storybook/manager-webpack5": "^6.3.12",
"@storybook/react": "^6.3.12",
"@storybook/theming": "^6.3.12",
"@storybook/addon-knobs": "^6.4.0",
"@storybook/addon-links": "^6.5.7",
"@storybook/addon-storyshots": "^6.5.7",
"@storybook/addon-storyshots-puppeteer": "^6.5.7",
"@storybook/addon-toolbars": "^6.5.7",
"@storybook/addons": "^6.5.7",
"@storybook/api": "^6.5.7",
"@storybook/builder-webpack5": "^6.5.7",
"@storybook/client-api": "^6.5.7",
"@storybook/components": "^6.5.7",
"@storybook/core": "^6.5.7",
"@storybook/core-events": "^6.5.7",
"@storybook/manager-webpack5": "^6.5.7",
"@storybook/react": "^6.5.7",
"@storybook/theming": "^6.5.7",
"@terminus-term/to-string-loader": "^1.1.7-beta.1",
"@testing-library/dom": "^8.13.0",
"@testing-library/jest-dom": "^5.16.4",
@ -307,7 +307,7 @@
"postcss-focus-visible": "^5.0.0",
"postcss-loader": "^6.1.1",
"postcss-modules": "^4.2.2",
"prettier": "^2.2.1",
"prettier": "2.2.1",
"process": "^0.11.10",
"protoc-gen-ts": "0.8.1",
"puppeteer": "^13.5.1",
@ -323,8 +323,8 @@
"socket.io": "^2.3.0",
"socket.io-client": "^2.3.0",
"speed-measure-webpack-plugin": "^1.5.0",
"storybook-addon-designs": "^6.2.0",
"storybook-dark-mode": "^1.0.8",
"storybook-addon-designs": "^6.2.1",
"storybook-dark-mode": "^1.1.0",
"string-width": "^4.2.0",
"style-loader": "^3.1.0",
"stylelint": "^14.3.0",
@ -478,6 +478,7 @@
"history": "4.5.1",
"cssnano": "4.1.10",
"webpack": "5",
"tslib": "2.1.0"
"tslib": "2.1.0",
"prettier": "2.2.1"
}
}

1987
yarn.lock

File diff suppressed because it is too large Load Diff