diff --git a/.storybook/preview.js b/.storybook/preview.js index 9dbedb5216d05..cefe0bed0de8c 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,9 +1,9 @@ -import { withFluentProvider, withStrictMode } from '@fluentui/react-storybook'; +import { withStrictMode } from '@fluentui/react-storybook'; import 'cypress-storybook/react'; import * as dedent from 'dedent'; /** @type {NonNullable} */ -export const decorators = [withFluentProvider, withStrictMode]; +export const decorators = [withStrictMode]; /** @type {import('@storybook/react').Parameters} */ export const parameters = { diff --git a/change/@fluentui-react-components-639d9023-4a53-403d-a3eb-fa50ca331ac6.json b/change/@fluentui-react-components-639d9023-4a53-403d-a3eb-fa50ca331ac6.json new file mode 100644 index 0000000000000..e2f8f4ec880d5 --- /dev/null +++ b/change/@fluentui-react-components-639d9023-4a53-403d-a3eb-fa50ca331ac6.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore(storybook): Theme picker as storybook addon", + "packageName": "@fluentui/react-components", + "email": "lingfangao@hotmail.com", + "dependentChangeType": "none" +} diff --git a/packages/react-components/.storybook/main.js b/packages/react-components/.storybook/main.js index 719751bc7e1a2..77e48491c3a06 100644 --- a/packages/react-components/.storybook/main.js +++ b/packages/react-components/.storybook/main.js @@ -9,7 +9,7 @@ module.exports = /** @type {Pick { const localConfig = { ...rootMain.webpackFinal(config, options) }; diff --git a/packages/react-components/.storybook/manager.js b/packages/react-components/.storybook/manager.js deleted file mode 100644 index 4a91dc7bc27dd..0000000000000 --- a/packages/react-components/.storybook/manager.js +++ /dev/null @@ -1,8 +0,0 @@ -import { addons } from '@storybook/addons'; -import theme from './theme'; - -addons.setConfig({ - showPanel: true, - panelPosition: 'right', - theme, -}); diff --git a/packages/react-components/.storybook/preview.js b/packages/react-components/.storybook/preview.js index f488f6cbdf571..f2b2e2f988aeb 100644 --- a/packages/react-components/.storybook/preview.js +++ b/packages/react-components/.storybook/preview.js @@ -24,4 +24,7 @@ const options = { export const decorators = [...rootPreview.decorators]; /** @type {typeof rootPreview.parameters} */ -export const parameters = { ...rootPreview.parameters, options }; +export const parameters = { + ...rootPreview.parameters, + options, +}; diff --git a/packages/react-components/package.json b/packages/react-components/package.json index baa5c0bd24990..b0c4a7db1da55 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -29,6 +29,7 @@ }, "devDependencies": { "@fluentui/eslint-plugin": "*", + "@fluentui/react-storybook-addon": "9.0.0-beta.0", "@fluentui/scripts": "^1.0.0", "@types/react": "16.9.42", "@types/react-dom": "16.9.10", diff --git a/packages/react-components/src/Migrations/utils.stories.tsx b/packages/react-components/src/Migrations/utils.stories.tsx index b3926d0fd37e2..023da5b7ae22d 100644 --- a/packages/react-components/src/Migrations/utils.stories.tsx +++ b/packages/react-components/src/Migrations/utils.stories.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -// eslint-disable-next-line import/no-extraneous-dependencies import { Source } from '@storybook/addon-docs'; import { makeStyles } from '@fluentui/react-make-styles'; diff --git a/packages/react-storybook-addon/README.md b/packages/react-storybook-addon/README.md index fdbac2cfe0721..9a052541499df 100644 --- a/packages/react-storybook-addon/README.md +++ b/packages/react-storybook-addon/README.md @@ -3,3 +3,38 @@ **Storybook addon for Fluent UI React [Fluent UI React](https://developer.microsoft.com/en-us/fluentui)** These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release. + +## ✨ Features + +**Toolbar/Tools** + +- adds fluent theme switcher + - ![Fluent Theme Switcher](https://user-images.githubusercontent.com/20744592/138872560-8ef40c25-193c-47db-a216-7c1e86fe8cda.png) + +## Getting Started + +### Installation + +> **NOTE:** this package is not being published yet + +```sh +yarn add -D @fluentui/react-storybook-addon +``` + +### Configuration + +Add following content to .storybook/main.js: + +```js +module.exports = { + addons: ['@fluentui/react-storybook-addon'], +}; +``` + +## Development + +1. Run inner loop from monorepo root `yarn workspace @fluentui/react-storybook-addon storybook` + + - > 💡 this will run `build` script that compiles addon implementation so it can be consumed by local storybook + +2. Every time you do any change to implementation, after you ran your local storybook you'll need to manually run `yarn workspace @fluentui/react-storybook-addon build` to reflect those changes diff --git a/packages/react-storybook-addon/package.json b/packages/react-storybook-addon/package.json index 3787ffc8e7534..6b45cf4e3119f 100644 --- a/packages/react-storybook-addon/package.json +++ b/packages/react-storybook-addon/package.json @@ -31,6 +31,8 @@ "react": "16.8.6" }, "dependencies": { + "@fluentui/react-theme": "9.0.0-beta.2", + "@fluentui/react-provider": "9.0.0-beta.3", "tslib": "^2.1.0" }, "peerDependencies": { diff --git a/packages/react-storybook-addon/src/components/.gitkeep b/packages/react-storybook-addon/src/components/.gitkeep deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/packages/react-storybook-addon/src/components/ThemePicker.tsx b/packages/react-storybook-addon/src/components/ThemePicker.tsx new file mode 100644 index 0000000000000..8d06603b3e645 --- /dev/null +++ b/packages/react-storybook-addon/src/components/ThemePicker.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import { IconButton, Icons, TooltipLinkList, WithTooltip } from '@storybook/components'; + +import { ThemeIds, themes, defaultTheme } from '../theme'; +import { THEME_ID } from '../constants'; +import { useGlobals } from '../hooks'; + +export interface ThemeSelectorItem { + id: string; + title: string; + onClick: () => void; + value: string; + active: boolean; +} + +function createThemeItems( + value: typeof themes, + changeTheme: (id: ThemeIds) => void, + getCurrentTheme: () => ThemeIds, +): ThemeSelectorItem[] { + return value.map(item => { + return { + id: item.id, + title: item.id === defaultTheme.id ? `${item.label} (Default)` : item.label, + onClick: () => { + changeTheme(item.id); + }, + value: item.id, + active: getCurrentTheme() === item.id, + }; + }); +} + +export const ThemePicker = () => { + const [globals, updateGlobals] = useGlobals(); + const selectedThemeId = globals[THEME_ID] ?? defaultTheme.id; + const selectedTheme = themes.find(entry => entry.id === selectedThemeId); + + const isActive = selectedThemeId !== defaultTheme.id; + + const setTheme = React.useCallback( + (id: ThemeIds) => { + updateGlobals({ [THEME_ID]: id }); + }, + [updateGlobals], + ); + + const renderTooltip = React.useCallback( + (props: { onHide: () => void }) => { + return ( + { + setTheme(id); + props.onHide(); + }, + () => selectedThemeId, + )} + /> + ); + }, + [selectedThemeId, setTheme], + ); + + return ( + <> + + + + Theme: {selectedTheme?.label} + + + + ); +}; diff --git a/packages/react-storybook-addon/src/constants.ts b/packages/react-storybook-addon/src/constants.ts index 3ef367bcec608..feac8295cbb2b 100644 --- a/packages/react-storybook-addon/src/constants.ts +++ b/packages/react-storybook-addon/src/constants.ts @@ -1 +1,2 @@ -// @TODO - add addon constants +export const ADDON_ID = 'storybook/fluentui-react-addon'; +export const THEME_ID = `${ADDON_ID}/theme` as const; diff --git a/packages/react-storybook-addon/src/decorators/withFluentProvider.tsx b/packages/react-storybook-addon/src/decorators/withFluentProvider.tsx new file mode 100644 index 0000000000000..c520b7dde643a --- /dev/null +++ b/packages/react-storybook-addon/src/decorators/withFluentProvider.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { StoryFn as StoryFunction } from '@storybook/addons'; + +import { themes, defaultTheme, FluentProvider } from '../theme'; +import { THEME_ID } from '../constants'; +import { FluentGlobals, FluentStoryContext } from '../hooks'; + +import { Theme } from '@fluentui/react-theme'; + +const getActiveFluentTheme = (globals: FluentGlobals) => { + const selectedThemeId = globals[THEME_ID]; + const { theme } = themes.find(value => value.id === selectedThemeId) ?? defaultTheme; + + return { theme }; +}; + +export const withFluentProvider = (StoryFn: StoryFunction, context: FluentStoryContext) => { + const { theme } = getActiveFluentTheme(context.globals); + + return ( + + {StoryFn()} + + ); +}; + +const FluentExampleContainer: React.FC<{ theme: Theme }> = props => { + const { theme } = props; + + const backgroundColor = theme.colorNeutralBackground1; + return
{props.children}
; +}; diff --git a/packages/react-storybook-addon/src/hooks.ts b/packages/react-storybook-addon/src/hooks.ts new file mode 100644 index 0000000000000..649100df48ae8 --- /dev/null +++ b/packages/react-storybook-addon/src/hooks.ts @@ -0,0 +1,20 @@ +import { useGlobals as useStorybookGlobals, Args as StorybookArgs } from '@storybook/api'; +import { StoryContext as StorybookContext } from '@storybook/addons'; + +import { THEME_ID } from './constants'; +import { ThemeIds } from './theme'; + +export interface FluentStoryContext extends StorybookContext { + globals: FluentGlobals; +} + +/** + * Extends the storybook globals object to include fluent specific propoerties + */ +export interface FluentGlobals extends StorybookArgs { + [THEME_ID]?: ThemeIds; +} + +export function useGlobals(): [FluentGlobals, (newGlobals: FluentGlobals) => void] { + return useStorybookGlobals(); +} diff --git a/packages/react-storybook-addon/src/preset/manager.ts b/packages/react-storybook-addon/src/preset/manager.ts index bb08716ee3e20..e052cd5e4c6ed 100644 --- a/packages/react-storybook-addon/src/preset/manager.ts +++ b/packages/react-storybook-addon/src/preset/manager.ts @@ -1 +1,13 @@ -// @TODO - Register the addon +import { addons, types } from '@storybook/addons'; + +import { ADDON_ID, THEME_ID } from '../constants'; +import { ThemePicker } from '../components/ThemePicker'; + +addons.register(ADDON_ID, () => { + addons.add(THEME_ID, { + title: 'Fluent Theme Picker', + type: types.TOOL, + match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), + render: ThemePicker, + }); +}); diff --git a/packages/react-storybook-addon/src/preset/preview.ts b/packages/react-storybook-addon/src/preset/preview.ts index 5540cbb6b136e..a4e2db0acf3d7 100644 --- a/packages/react-storybook-addon/src/preset/preview.ts +++ b/packages/react-storybook-addon/src/preset/preview.ts @@ -9,4 +9,6 @@ * https://storybook.js.org/docs/react/writing-stories/decorators#gatsby-focus-wrapper */ -export const decorators = []; +import { withFluentProvider } from '../decorators/withFluentProvider'; + +export const decorators = [withFluentProvider]; diff --git a/packages/react-storybook-addon/src/theme.ts b/packages/react-storybook-addon/src/theme.ts new file mode 100644 index 0000000000000..04ec2f84d423c --- /dev/null +++ b/packages/react-storybook-addon/src/theme.ts @@ -0,0 +1,27 @@ +import { + webLightTheme, + webDarkTheme, + webHighContrastTheme, + teamsLightTheme, + teamsDarkTheme, + teamsHighContrastTheme, + Theme, +} from '@fluentui/react-theme'; + +export { FluentProvider } from '@fluentui/react-provider'; + +export const themes = [ + { id: 'web-light', label: 'Web Light', theme: webLightTheme }, + { id: 'web-dark', label: 'Web Dark', theme: webDarkTheme }, + { id: 'web-high-contrast', label: 'Web High Contrast', theme: webHighContrastTheme }, + { id: 'teams-light', label: 'Teams Light', theme: teamsLightTheme }, + { id: 'teams-dark', label: 'Teams Dark', theme: teamsDarkTheme }, + { id: 'teams-high-contrast', label: 'Teams High Contrast', theme: teamsHighContrastTheme }, +] as const; + +export const defaultTheme = themes[0]; + +export type ThemeIds = typeof themes[number]['id']; +export type ThemeLabels = typeof themes[number]['label']; + +export { Theme }; diff --git a/packages/react-storybook/README.md b/packages/react-storybook/README.md index 41e2f9226846d..bdc825fa64b22 100644 --- a/packages/react-storybook/README.md +++ b/packages/react-storybook/README.md @@ -22,8 +22,8 @@ You need to register fluentui decorators on your particular level (global/story/ // @filename: .storybook/preview.js import { withKnobs } from '@storybook/addon-knobs'; -import { withFluentProvider, withStrictMode } from '@fluentui/react-storybook'; +import { withStrictMode } from '@fluentui/react-storybook'; // Register decorators on global level -export const decorators = [withKnobs, withFluentProvider, withStrictMode]; +export const decorators = [withKnobs, withStrictMode]; ``` diff --git a/packages/react-storybook/etc/react-storybook.api.md b/packages/react-storybook/etc/react-storybook.api.md index 4a3d4b6703734..e9d37726e6504 100644 --- a/packages/react-storybook/etc/react-storybook.api.md +++ b/packages/react-storybook/etc/react-storybook.api.md @@ -6,13 +6,9 @@ import * as React_2 from 'react'; -// @public (undocumented) -export const withFluentProvider: (...args: any) => any; - // @public (undocumented) export const withStrictMode: (storyFn: () => React_2.ReactNode) => JSX.Element; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/react-storybook/src/decorators/index.ts b/packages/react-storybook/src/decorators/index.ts index ff044dce8afc1..b5502b4153487 100644 --- a/packages/react-storybook/src/decorators/index.ts +++ b/packages/react-storybook/src/decorators/index.ts @@ -1,2 +1 @@ -export * from './withFluentProvider'; export * from './withStrictMode'; diff --git a/packages/react-storybook/src/decorators/withFluentProvider.tsx b/packages/react-storybook/src/decorators/withFluentProvider.tsx deleted file mode 100644 index 2d55636bad8f8..0000000000000 --- a/packages/react-storybook/src/decorators/withFluentProvider.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { makeDecorator } from '@storybook/addons'; -import { FluentProvider } from '@fluentui/react-provider'; -import * as React from 'react'; - -import { useFluentTheme } from '../knobs/useFluentTheme'; - -const ProviderWrapper: React.FunctionComponent = props => { - const { theme } = useFluentTheme(); - - return {props.children}; -}; - -export const withFluentProvider = makeDecorator({ - name: 'withFluentProvider', - parameterName: 'theme', - skipIfNoParametersOrOptions: false, - wrapper: (storyFn, context) => { - return {storyFn(context)}; - }, -}); diff --git a/packages/react-storybook/src/index.test.ts b/packages/react-storybook/src/index.test.ts index 3f2fe8451a862..446d13e865c7f 100644 --- a/packages/react-storybook/src/index.test.ts +++ b/packages/react-storybook/src/index.test.ts @@ -1,9 +1,9 @@ -import { withFluentProvider, withStrictMode } from './index'; +import { withStrictMode } from './index'; describe(`public api`, () => { describe(`decorators`, () => { it(`should work`, () => { - const decorators = [withFluentProvider, withStrictMode]; + const decorators = [withStrictMode]; // @TODO - added proper tests expect(decorators).toBeDefined(); diff --git a/packages/storybook/src/index.ts b/packages/storybook/src/index.ts index 3a4c10f824da1..2eeb81228a281 100644 --- a/packages/storybook/src/index.ts +++ b/packages/storybook/src/index.ts @@ -1,2 +1,2 @@ export * from './decorators/index'; -export { withFluentProvider, withStrictMode } from '@fluentui/react-storybook'; +export { withStrictMode } from '@fluentui/react-storybook';