Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSF: Add addon-themes for testing to sandboxes & main storybook #28765

Merged
merged 13 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions code/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ const config: StorybookConfig = {
directory: '../addons/toolbars/template/stories',
titlePrefix: 'addons/toolbars',
},
{
directory: '../addons/themes/template/stories',
titlePrefix: 'addons/themes',
},
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
{
directory: '../addons/onboarding/src',
titlePrefix: 'addons/onboarding',
Expand All @@ -83,6 +87,7 @@ const config: StorybookConfig = {
],
addons: [
'@storybook/addon-links',
'@storybook/addon-themes',
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-storysource',
Expand Down
5 changes: 4 additions & 1 deletion code/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export const decorators = [
* This decorator renders the stories side-by-side, stacked or default based on the theme switcher in the toolbar
*/
(StoryFn, { globals, playFunction, args, storyGlobals, parameters }) => {
let theme = globals.theme;
let theme = globals.sb_theme;
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
let showPlayFnNotice = false;

// this makes the decorator be out of 'phase' with the actually selected theme in the toolbar
Expand Down Expand Up @@ -315,6 +315,9 @@ export const parameters = {
viewport: {
options: MINIMAL_VIEWPORTS,
},
themes: {
disable: true,
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
},
backgrounds: {
options: {
light: { name: 'light', value: '#edecec' },
Expand Down
8 changes: 4 additions & 4 deletions code/addons/controls/template/stories/conditional.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ export const ToggleExpandCollapse = {

export const GlobalBased = {
argTypes: {
ifThemeExists: { control: 'text', if: { global: 'theme' } },
ifThemeNotExists: { control: 'text', if: { global: 'theme', exists: false } },
ifLightTheme: { control: 'text', if: { global: 'theme', eq: 'light' } },
ifNotLightTheme: { control: 'text', if: { global: 'theme', neq: 'light' } },
ifThemeExists: { control: 'text', if: { global: 'sb_theme' } },
ifThemeNotExists: { control: 'text', if: { global: 'sb_theme', exists: false } },
ifLightTheme: { control: 'text', if: { global: 'sb_theme', eq: 'light' } },
ifNotLightTheme: { control: 'text', if: { global: 'sb_theme', neq: 'light' } },
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const Disabled: Story = {

export const Hovered: Story = {
...Done,
globals: { theme: 'light' },
globals: { sb_theme: 'light' },
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.hover(canvas.getByRole('button'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const meta = {
),
],
parameters: { layout: 'fullscreen' },
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
globals: { theme: 'light' },
globals: { sb_theme: 'light' },
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
args: {
calls: new Map(getCalls(CallStates.DONE).map((call) => [call.id, call])),
controls: SubnavStories.args.controls,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default {
onSuccess: fn(),
},
globals: {
theme: 'light',
sb_theme: 'light',
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
},
};

Expand Down
1 change: 1 addition & 0 deletions code/addons/themes/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const DEFAULT_ADDON_STATE: ThemeAddonState = {

export interface ThemeParameters {
themeOverride?: string;
disable?: boolean;
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
}

export const DEFAULT_THEME_PARAMETERS: ThemeParameters = {};
Expand Down
4 changes: 2 additions & 2 deletions code/addons/themes/src/decorators/class-name.decorator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const DEFAULT_ELEMENT_SELECTOR = 'html';
const classStringToArray = (classString: string) => classString.split(' ').filter(Boolean);

// TODO check with @kasperpeulen: change the types so they can be correctly inferred from context e.g. <Story extends (...args: any[]) => any>
export const withThemeByClassName = <TRenderer extends Renderer = any>({
export const withThemeByClassName = <TRenderer extends Renderer = Renderer>({
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
themes,
defaultTheme,
parentSelector = DEFAULT_ELEMENT_SELECTOR,
Expand Down Expand Up @@ -47,7 +47,7 @@ export const withThemeByClassName = <TRenderer extends Renderer = any>({
if (newThemeClasses.length > 0) {
parentElement.classList.add(...newThemeClasses);
}
}, [themeOverride, selected, parentSelector]);
}, [themeOverride, selected]);
ndelangen marked this conversation as resolved.
Show resolved Hide resolved

return storyFn();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const withThemeByDataAttribute = <TRenderer extends Renderer = any>({
if (parentElement) {
parentElement.setAttribute(attributeName, themes[themeKey]);
}
}, [themeOverride, selected, parentSelector, attributeName]);
}, [themeOverride, selected]);
ndelangen marked this conversation as resolved.
Show resolved Hide resolved

return storyFn();
};
Expand Down
2 changes: 1 addition & 1 deletion code/addons/themes/src/decorators/provider.decorator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const withThemeFromJSXProvider = <TRenderer extends Renderer = any>({
const pairs = Object.entries(themes);

return pairs.length === 1 ? pluckThemeFromKeyPairTuple(pairs[0]) : themes[selectedThemeName];
}, [themes, selected, themeOverride]);
}, [selected, themeOverride]);
ndelangen marked this conversation as resolved.
Show resolved Hide resolved

if (!Provider) {
return (
Expand Down
7 changes: 3 additions & 4 deletions code/addons/themes/src/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Renderer, ProjectAnnotations } from 'storybook/internal/types';
import { GLOBAL_KEY } from './constants';
import { GLOBAL_KEY as KEY } from './constants';

export const globals: ProjectAnnotations<Renderer>['globals'] = {
// Required to make sure SB picks this up from URL params
[GLOBAL_KEY]: '',
export const initialGlobals: ProjectAnnotations<Renderer>['initialGlobals'] = {
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
[KEY]: '',
};
85 changes: 47 additions & 38 deletions code/addons/themes/src/theme-switcher.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Fragment, useMemo } from 'react';
import React from 'react';
import {
useAddonState,
useChannel,
Expand All @@ -17,6 +17,7 @@ import {
THEMING_EVENTS,
DEFAULT_ADDON_STATE,
DEFAULT_THEME_PARAMETERS,
GLOBAL_KEY as KEY,
} from './constants';

const IconButtonLabel = styled.div(({ theme }) => ({
Expand All @@ -27,11 +28,11 @@ const hasMultipleThemes = (themesList: ThemeAddonState['themesList']) => themesL
const hasTwoThemes = (themesList: ThemeAddonState['themesList']) => themesList.length === 2;

export const ThemeSwitcher = React.memo(function ThemeSwitcher() {
const { themeOverride } = useParameter<ThemeParameters>(
const { themeOverride, disable } = useParameter<ThemeParameters>(
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
PARAM_KEY,
DEFAULT_THEME_PARAMETERS
) as ThemeParameters;
const [{ theme: selected }, updateGlobals] = useGlobals();
const [{ theme: selected }, updateGlobals, storyGlobals] = useGlobals();

const channel = addons.getChannel();
const fromLast = channel.last(THEMING_EVENTS.REGISTER_THEMES);
Expand All @@ -45,6 +46,8 @@ export const ThemeSwitcher = React.memo(function ThemeSwitcher() {
initializeThemeState
);

const isLocked = KEY in storyGlobals || !!themeOverride;
ndelangen marked this conversation as resolved.
Show resolved Hide resolved

useChannel({
[THEMING_EVENTS.REGISTER_THEMES]: ({ themes, defaultTheme }) => {
updateState((state) => ({
Expand All @@ -55,21 +58,24 @@ export const ThemeSwitcher = React.memo(function ThemeSwitcher() {
},
});

const label = useMemo(() => {
if (themeOverride) {
return <>Story override</>;
}

const themeName = selected || themeDefault;
const themeName = selected || themeDefault;
let label = '';
if (isLocked) {
label = 'Story override';
} else if (themeName) {
label = `${themeName} theme`;
}

return themeName && <>{`${themeName} theme`}</>;
}, [themeOverride, themeDefault, selected]);
if (disable) {
return null;
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
}

if (hasTwoThemes(themesList)) {
const currentTheme = selected || themeDefault;
const alternateTheme = themesList.find((theme) => theme !== currentTheme);
return (
<IconButton
disabled={isLocked}
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
key={THEME_SWITCHER_ID}
active={!themeOverride}
title="Theme"
Expand All @@ -78,40 +84,43 @@ export const ThemeSwitcher = React.memo(function ThemeSwitcher() {
}}
>
<PaintBrushIcon />
{label && <IconButtonLabel>{label}</IconButtonLabel>}
{label ? <IconButtonLabel>{label}</IconButtonLabel> : null}
</IconButton>
);
}

if (hasMultipleThemes(themesList)) {
return (
<Fragment>
<WithTooltip
placement="top"
trigger="click"
closeOnOutsideClick
tooltip={({ onHide }) => {
return (
<TooltipLinkList
links={themesList.map((theme) => ({
id: theme,
title: theme,
active: selected === theme,
onClick: () => {
updateGlobals({ theme });
onHide();
},
}))}
/>
);
}}
<WithTooltip
placement="top"
trigger="click"
closeOnOutsideClick
tooltip={({ onHide }) => {
return (
<TooltipLinkList
links={themesList.map((theme) => ({
id: theme,
title: theme,
active: selected === theme,
onClick: () => {
updateGlobals({ theme });
onHide();
},
}))}
/>
);
}}
>
<IconButton
key={THEME_SWITCHER_ID}
active={!themeOverride}
title="Theme"
disabled={isLocked}
>
<IconButton key={THEME_SWITCHER_ID} active={!themeOverride} title="Theme">
<PaintBrushIcon />
{label && <IconButtonLabel>{label}</IconButtonLabel>}
</IconButton>
</WithTooltip>
</Fragment>
<PaintBrushIcon />
{label && <IconButtonLabel>{label}</IconButtonLabel>}
</IconButton>
</WithTooltip>
);
}

Expand Down
93 changes: 93 additions & 0 deletions code/addons/themes/template/stories/decorators.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { global as globalThis } from '@storybook/global';
import {
withThemeByClassName,
withThemeByDataAttribute,
withThemeFromJSXProvider,
} from '@storybook/addon-themes';
import { useEffect } from 'storybook/internal/preview-api';

const cleanup = () => {
const existing = globalThis.document.querySelector('style[data-theme-css]');
if (existing) {
existing.remove();
}
};

const addStyleSheetDecorator = (storyFn: any) => {
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
useEffect(() => {
cleanup();

const sheet = globalThis.document.createElement('style');
sheet.setAttribute('data-theme-css', '');
sheet.textContent = `
[data-theme="theme-a"], .theme-a {
background-color: white;
color: black;
}
[data-theme="theme-b"], .theme-b {
background-color: black;
color: white;
}
`;

globalThis.document.body.appendChild(sheet);

return cleanup;
});

return storyFn();
};

export default {
component: globalThis.Components.Pre,
args: {
text: 'Testing the themes',
},
parameters: {
chromatic: { disable: true },
themes: { disable: false },
},
decorators: [addStyleSheetDecorator],
};

export const WithThemeByClassName = {
globals: {},
decorators: [
withThemeByClassName({
defaultTheme: 'a',
themes: { a: 'theme-a', b: 'theme-b' },
parentSelector: '#storybook-root > *',
}),
],
};

export const WithThemeByDataAttribute = {
globals: {},
decorators: [
withThemeByDataAttribute({
defaultTheme: 'a',
themes: { a: 'theme-a', b: 'theme-b' },
parentSelector: '#storybook-root > *',
}),
],
};

export const WithThemeFromJSXProvider = {
globals: {},
decorators: [
withThemeFromJSXProvider({
defaultTheme: 'a',
themes: { a: { custom: 'theme-a' }, b: { custom: 'theme-b' } },
Provider: ({ theme, children }: any) => {
// this is not was a normal provider looks like obviously, but this needs to work in non-react as well
// the timeout is to wait for the render to complete, as it's not possible to use the useEffect hook here
setTimeout(() => {
const element = globalThis.document.querySelector('#storybook-root > *');
element?.classList.remove('theme-a', 'theme-b');
element?.classList.add(theme.custom);
}, 16);
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
return children;
},
}),
],
};
Loading