Skip to content

Commit dd30efd

Browse files
authored
Merge pull request #411 from chromaui/highlight-ignored-elements
Add tool to highlight ignored elements
2 parents cb63bab + c5df0fb commit dd30efd

15 files changed

Lines changed: 459 additions & 13 deletions

.storybook/preview.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import { ManagerContext } from 'storybook/manager-api';
21
import type { Decorator, Loader, Preview } from '@storybook/react-vite';
2+
import { graphql, HttpResponse } from 'msw';
3+
import { initialize, mswLoader } from 'msw-storybook-addon';
4+
import React from 'react';
5+
import { ManagerContext } from 'storybook/manager-api';
36
import { fn } from 'storybook/test';
47
import {
5-
Global,
6-
ThemeProvider,
78
convert,
89
createReset,
10+
Global,
911
styled,
12+
ThemeProvider,
1013
themes,
1114
useTheme,
1215
} from 'storybook/theming';
13-
import { HttpResponse, graphql } from 'msw';
14-
import { initialize, mswLoader } from 'msw-storybook-addon';
15-
import React from 'react';
1616

1717
import { AuthProvider } from '../src/AuthContext';
1818
import { baseModes } from '../src/modes';
@@ -234,6 +234,7 @@ const preview: Preview = {
234234
},
235235
chromatic: {
236236
modes: baseModes,
237+
ignoreSelectors: ['video'],
237238
},
238239
controls: {
239240
matchers: {

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@
2222
"require": "./dist/index.js",
2323
"import": "./dist/index.mjs"
2424
},
25-
"./manager": "./dist/manager.js",
25+
"./manager": "./dist/manager.mjs",
26+
"./package.json": "./package.json",
2627
"./preset": "./dist/preset.js",
27-
"./package.json": "./package.json"
28+
"./preview": {
29+
"types": "./dist/preview.d.ts",
30+
"default": "./dist/preview.mjs"
31+
}
2832
},
2933
"main": "dist/index.js",
3034
"files": [
@@ -153,6 +157,9 @@
153157
],
154158
"nodeEntries": [
155159
"./src/preset.ts"
160+
],
161+
"previewEntries": [
162+
"./src/preview.ts"
156163
]
157164
},
158165
"msw": {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
import React from 'react';
3+
import { fn } from 'storybook/test';
4+
5+
import { GlobalIgnoreToggleButton } from './GlobalIgnoreToggle';
6+
7+
type StoryArgs = React.ComponentProps<typeof GlobalIgnoreToggleButton>;
8+
9+
const StatefulButton = (args: StoryArgs) => {
10+
const [enabled, setEnabled] = React.useState(args.enabled);
11+
12+
React.useEffect(() => {
13+
setEnabled(args.enabled);
14+
}, [args.enabled]);
15+
16+
return (
17+
<GlobalIgnoreToggleButton
18+
{...args}
19+
enabled={enabled}
20+
onToggle={() => {
21+
args.onToggle();
22+
setEnabled((current) => !current);
23+
}}
24+
/>
25+
);
26+
};
27+
28+
const meta = {
29+
title: 'components/GlobalIgnoreToggle',
30+
component: GlobalIgnoreToggleButton,
31+
render: (args) => <StatefulButton {...args} />,
32+
args: {
33+
enabled: false,
34+
ignoreCount: 3,
35+
locked: false,
36+
onToggle: fn(),
37+
},
38+
} satisfies Meta<typeof GlobalIgnoreToggleButton>;
39+
40+
export default meta;
41+
type Story = StoryObj<typeof meta>;
42+
43+
export const Hidden: Story = {
44+
args: {
45+
ignoreCount: 0,
46+
},
47+
};
48+
49+
export const Inactive: Story = {};
50+
51+
export const Active: Story = {
52+
args: {
53+
enabled: true,
54+
},
55+
};
56+
57+
export const LockedByStoryGlobals: Story = {
58+
args: {
59+
enabled: true,
60+
locked: true,
61+
},
62+
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { ContrastIgnoredIcon } from '@storybook/icons';
2+
import React, { useState } from 'react';
3+
import { IconButton } from 'storybook/internal/components';
4+
import { useChannel, useGlobals } from 'storybook/manager-api';
5+
6+
import { HIGHLIGHT_IGNORED_COUNT, HIGHLIGHT_IGNORED_PARAM } from '../constants';
7+
8+
type GlobalIgnoreToggleButtonProps = {
9+
enabled: boolean;
10+
ignoreCount: number;
11+
locked: boolean;
12+
onToggle: () => void;
13+
};
14+
15+
export const GlobalIgnoreToggleButton = ({
16+
enabled,
17+
ignoreCount,
18+
locked,
19+
onToggle,
20+
}: GlobalIgnoreToggleButtonProps) => {
21+
return ignoreCount === 0 ? null : (
22+
<IconButton
23+
key={HIGHLIGHT_IGNORED_PARAM}
24+
active={enabled}
25+
ariaLabel={
26+
locked
27+
? `Highlights ${enabled ? 'enabled' : 'disabled'} by story globals`
28+
: `${enabled ? 'Hide' : 'Show'} ignored areas`
29+
}
30+
padding="small"
31+
variant="ghost"
32+
disabled={locked}
33+
onClick={onToggle}
34+
>
35+
<ContrastIgnoredIcon />
36+
{ignoreCount}
37+
</IconButton>
38+
);
39+
};
40+
41+
export const GlobalIgnoreToggle = () => {
42+
const [globals, updateGlobals, storyGlobals] = useGlobals();
43+
const [ignoreCount, setIgnoreCount] = useState(0);
44+
const enabled = !!globals[HIGHLIGHT_IGNORED_PARAM];
45+
const locked = HIGHLIGHT_IGNORED_PARAM in storyGlobals;
46+
47+
useChannel({ [HIGHLIGHT_IGNORED_COUNT]: setIgnoreCount }, []);
48+
49+
return (
50+
<GlobalIgnoreToggleButton
51+
enabled={enabled}
52+
ignoreCount={ignoreCount}
53+
locked={locked}
54+
onToggle={() =>
55+
updateGlobals({ [HIGHLIGHT_IGNORED_PARAM]: !globals[HIGHLIGHT_IGNORED_PARAM] })
56+
}
57+
/>
58+
);
59+
};

src/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ export const TELEMETRY = `${ADDON_ID}/telemetry`;
2727
export const ENABLE_FILTER = `${ADDON_ID}/enableFilter`;
2828
export const REMOVE_ADDON = `${ADDON_ID}/removeAddon`;
2929
export const PARAM_KEY = 'chromatic';
30+
export const HIGHLIGHT_IGNORED_PARAM = 'highlightIgnored';
31+
export const HIGHLIGHT_IGNORED_ID = `${ADDON_ID}/highlightIgnored`;
32+
export const HIGHLIGHT_IGNORED_COUNT = `${ADDON_ID}/highlightIgnored/count`;
33+
export const HIGHLIGHT_IGNORED_SELECT = `${ADDON_ID}/highlightIgnored/select`;
34+
export const HIGHLIGHT_IGNORED_DEFAULT_SELECTORS = [
35+
'[data-chromatic="ignore"]',
36+
'[class~="chromatic-ignore"]',
37+
];
3038

3139
export const FETCH_ABORTED = `${ADDON_ID}/ChannelFetch/aborted`;
3240
export const FETCH_REQUEST = `${ADDON_ID}ChannelFetch/request`;

src/dev.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ import config from './preset.ts';
55
export default {
66
...config,
77
managerEntries: [fileURLToPath(import.meta.resolve('./manager.tsx'))],
8+
previewAnnotations: [fileURLToPath(import.meta.resolve('./preview.ts'))],
89
};

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { definePreviewAddon } from 'storybook/internal/csf';
22

3+
import * as addonAnnotations from './preview.ts';
34
import type { ChromaticTypes } from './types';
45

5-
export default () => definePreviewAddon<ChromaticTypes>({});
6+
export default () => definePreviewAddon<ChromaticTypes>(addonAnnotations);

src/manager.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,38 @@
11
import React from 'react';
2+
import { type ClickEventDetails } from 'storybook/highlight';
23
import { type Addon_TestProviderType, Addon_TypesEnum } from 'storybook/internal/types';
34
import { addons, experimental_getStatusStore } from 'storybook/manager-api';
45

5-
import { ADDON_ID, PANEL_ID, PARAM_KEY, TEST_PROVIDER_ID } from './constants.ts';
6+
import { GlobalIgnoreToggle } from './components/GlobalIgnoreToggle.tsx';
7+
import {
8+
ADDON_ID,
9+
HIGHLIGHT_IGNORED_DEFAULT_SELECTORS,
10+
HIGHLIGHT_IGNORED_SELECT,
11+
PANEL_ID,
12+
PARAM_KEY,
13+
TEST_PROVIDER_ID,
14+
} from './constants.ts';
615
import { Panel } from './Panel';
716
import { TestProviderRender } from './TestProviderRender';
817

918
addons.register(ADDON_ID, (api) => {
19+
api.on(HIGHLIGHT_IGNORED_SELECT, (itemId: string, details: ClickEventDetails) => {
20+
const isDefaultSelector = HIGHLIGHT_IGNORED_DEFAULT_SELECTORS.includes(details.selectors[0]);
21+
window.open(
22+
isDefaultSelector
23+
? 'https://www.chromatic.com/docs/ignoring-elements/#ignoring-elements-inline'
24+
: 'https://www.chromatic.com/docs/ignoring-elements/#ignoring-elements-via-test-configuration',
25+
'_blank'
26+
);
27+
});
28+
29+
addons.add(`${ADDON_ID}/ignore-highlight-tool`, {
30+
type: Addon_TypesEnum.TOOL,
31+
title: 'Highlight ignored areas',
32+
match: ({ viewMode }) => viewMode === 'story',
33+
render: () => <GlobalIgnoreToggle />,
34+
});
35+
1036
addons.add(PANEL_ID, {
1137
type: Addon_TypesEnum.PANEL,
1238
title: 'Visual tests',

src/preset.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ const chromaticLogger = createLogger(undefined, CONFIG_OVERRIDES);
5353
function managerEntries(entry: string[] = []) {
5454
return [...entry, require.resolve('./manager.mjs')];
5555
}
56+
function previewAnnotations(entry: string[] = []) {
57+
return [...entry, require.resolve('./preview.mjs')];
58+
}
5659

5760
// Load the addon version from the package.json file, once.
5861
let getAddonVersion = async (): Promise<string | null> => {
@@ -281,6 +284,7 @@ async function serverChannel(channel: Channel, options: Options & { configFile?:
281284

282285
const config = {
283286
managerEntries,
287+
previewAnnotations,
284288
experimental_serverChannel: serverChannel,
285289
staticDirs: async (inputDirs: string[]) => [
286290
...inputDirs,

src/preview.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { HIGHLIGHT, REMOVE_HIGHLIGHT } from 'storybook/highlight';
2+
import { useChannel, useEffect } from 'storybook/preview-api';
3+
4+
import {
5+
HIGHLIGHT_IGNORED_COUNT,
6+
HIGHLIGHT_IGNORED_ID,
7+
HIGHLIGHT_IGNORED_PARAM,
8+
} from './constants';
9+
import { getIgnoreHighlightOptions } from './utils/ignoreHighlight';
10+
11+
const WithIgnoreHighlight = (Story: any, { globals, parameters, id }: any) => {
12+
const enabled = !!globals[HIGHLIGHT_IGNORED_PARAM];
13+
const emit = useChannel({});
14+
const highlightOptions = getIgnoreHighlightOptions(parameters.chromatic);
15+
const selectorKey = highlightOptions?.selectors.join('\n') ?? '';
16+
17+
useEffect(() => {
18+
emit(REMOVE_HIGHLIGHT, HIGHLIGHT_IGNORED_ID);
19+
if (!highlightOptions?.selectors.length) {
20+
emit(HIGHLIGHT_IGNORED_COUNT, 0);
21+
return;
22+
}
23+
if (enabled) {
24+
emit(HIGHLIGHT, highlightOptions);
25+
}
26+
27+
const root = document.getElementById('storybook-root');
28+
const elements = highlightOptions.selectors.reduce((acc, selector) => {
29+
// Elements matching the selector, excluding storybook elements and their descendants.
30+
// Necessary to find portaled elements (e.g. children of `body`).
31+
document
32+
.querySelectorAll(
33+
`:is(${selector}):not([id^="storybook-"], [id^="storybook-"] *, [class^="sb-"], [class^="sb-"] *)`
34+
)
35+
.forEach((element) => acc.add(element));
36+
37+
// Elements matching the selector inside the storybook root, as these were excluded above.
38+
(root?.querySelectorAll(selector) ?? []).forEach((element) => acc.add(element));
39+
40+
return acc;
41+
}, new Set());
42+
emit(HIGHLIGHT_IGNORED_COUNT, elements.size);
43+
44+
return () => emit(REMOVE_HIGHLIGHT, HIGHLIGHT_IGNORED_ID);
45+
}, [emit, highlightOptions, id, selectorKey, enabled]);
46+
47+
return Story();
48+
};
49+
50+
export const decorators = [WithIgnoreHighlight];
51+
52+
export const initialGlobals = {
53+
[HIGHLIGHT_IGNORED_PARAM]: false,
54+
};

0 commit comments

Comments
 (0)