Skip to content

Commit fd63ccf

Browse files
committed
fix: add type-safety to Storybook preview.jsx file
1 parent 9006b21 commit fd63ccf

File tree

1 file changed

+123
-100
lines changed

1 file changed

+123
-100
lines changed

site/.storybook/preview.jsx

Lines changed: 123 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,147 @@
1+
// @ts-check
2+
/**
3+
* @typedef {import("react").ReactElement} ReactElement
4+
* @typedef {import("react").PropsWithChildren} PropsWithChildren
5+
* @typedef {import("react").FC<PropsWithChildren>} FC
6+
* @typedef {import("@storybook/react").StoryContext} StoryContext
7+
*
8+
* @typedef {(Story: FC, Context: StoryContext) => React.JSX.Element} Decorator A
9+
* Storybook decorator function used to inject baseline data dependencies into
10+
* our React components during testing.
11+
*/
12+
import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";
113
import CssBaseline from "@mui/material/CssBaseline";
214
import {
3-
StyledEngineProvider,
4-
ThemeProvider as MuiThemeProvider,
15+
ThemeProvider as MuiThemeProvider,
16+
StyledEngineProvider,
517
} from "@mui/material/styles";
6-
import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";
718
import { DecoratorHelpers } from "@storybook/addon-themes";
8-
import { withRouter } from "storybook-addon-remix-react-router";
9-
import { StrictMode } from "react";
10-
import { parseQueryArgs, QueryClient, QueryClientProvider } from "react-query";
19+
import isChromatic from "chromatic/isChromatic";
20+
import React, { StrictMode } from "react";
1121
import { HelmetProvider } from "react-helmet-async";
12-
import themes from "theme";
22+
import { parseQueryArgs, QueryClient, QueryClientProvider } from "react-query";
23+
import { withRouter } from "storybook-addon-remix-react-router";
1324
import "theme/globalFonts";
14-
import isChromatic from "chromatic/isChromatic";
25+
import themes from "../src/theme";
1526

1627
DecoratorHelpers.initializeThemeState(Object.keys(themes), "dark");
1728

18-
export const decorators = [
19-
withRouter,
20-
withQuery,
21-
(Story) => {
22-
return (
23-
<HelmetProvider>
24-
<Story />
25-
</HelmetProvider>
26-
);
27-
},
28-
(Story, context) => {
29-
const selectedTheme = DecoratorHelpers.pluckThemeFromContext(context);
30-
const { themeOverride } = DecoratorHelpers.useThemeParameters();
31-
const selected = themeOverride || selectedTheme || "dark";
32-
33-
return (
34-
<StrictMode>
35-
<StyledEngineProvider injectFirst>
36-
<MuiThemeProvider theme={themes[selected]}>
37-
<EmotionThemeProvider theme={themes[selected]}>
38-
<CssBaseline />
39-
<Story />
40-
</EmotionThemeProvider>
41-
</MuiThemeProvider>
42-
</StyledEngineProvider>
43-
</StrictMode>
44-
);
45-
},
46-
];
29+
/**@type {readonly Decorator[]} */
30+
export const decorators = [withRouter, withQuery, withHelmet, withTheme];
4731

4832
export const parameters = {
49-
options: {
50-
storySort: {
51-
method: "alphabetical",
52-
order: ["design", "pages", "modules", "components"],
53-
locales: "en-US",
54-
},
55-
},
56-
controls: {
57-
expanded: true,
58-
matchers: {
59-
color: /(background|color)$/i,
60-
date: /Date$/,
61-
},
62-
},
63-
viewport: {
64-
viewports: {
65-
ipad: {
66-
name: "iPad Mini",
67-
styles: {
68-
height: "1024px",
69-
width: "768px",
70-
},
71-
type: "tablet",
72-
},
73-
terminal: {
74-
name: "Terminal",
75-
styles: {
76-
height: "400",
77-
width: "400",
78-
},
79-
},
80-
},
81-
},
33+
options: {
34+
storySort: {
35+
method: "alphabetical",
36+
order: ["design", "pages", "modules", "components"],
37+
locales: "en-US",
38+
},
39+
},
40+
controls: {
41+
expanded: true,
42+
matchers: {
43+
color: /(background|color)$/i,
44+
date: /Date$/,
45+
},
46+
},
47+
viewport: {
48+
viewports: {
49+
ipad: {
50+
name: "iPad Mini",
51+
styles: {
52+
height: "1024px",
53+
width: "768px",
54+
},
55+
type: "tablet",
56+
},
57+
terminal: {
58+
name: "Terminal",
59+
styles: {
60+
height: "400",
61+
width: "400",
62+
},
63+
},
64+
},
65+
},
8266
};
8367

68+
/**
69+
* There's a mismatch for the React Helmet types that causes issues when
70+
* mounting the component in JS files only. Have to do type assertion, which is
71+
* especially ugly in JSDoc
72+
*/
73+
const SafeHelmetProvider = /** @type {FC} */ (
74+
/** @type {unknown} */ (HelmetProvider)
75+
);
76+
77+
/** @type {Decorator} */
78+
function withHelmet(Story) {
79+
return (
80+
<SafeHelmetProvider>
81+
<Story />
82+
</SafeHelmetProvider>
83+
);
84+
}
85+
86+
/** @type {Decorator} */
8487
function withQuery(Story, { parameters }) {
85-
const queryClient = new QueryClient({
86-
defaultOptions: {
87-
queries: {
88-
staleTime: Infinity,
89-
retry: false,
90-
},
91-
},
92-
});
88+
const queryClient = new QueryClient({
89+
defaultOptions: {
90+
queries: {
91+
staleTime: Number.POSITIVE_INFINITY,
92+
retry: false,
93+
},
94+
},
95+
});
96+
97+
if (parameters.queries) {
98+
for (const query of parameters.queries) {
99+
if (query.data instanceof Error) {
100+
// This is copied from setQueryData() but sets the error.
101+
const cache = queryClient.getQueryCache();
102+
const parsedOptions = parseQueryArgs(query.key);
103+
const defaultedOptions = queryClient.defaultQueryOptions(parsedOptions);
104+
const cachedQuery = cache.build(queryClient, defaultedOptions);
105+
// Set manual data so react-query will not try to refetch.
106+
cachedQuery.setData(undefined, { manual: true });
107+
cachedQuery.setState({ error: query.data });
108+
} else {
109+
queryClient.setQueryData(query.key, query.data);
110+
}
111+
}
112+
}
113+
114+
return (
115+
<QueryClientProvider client={queryClient}>
116+
<Story />
117+
</QueryClientProvider>
118+
);
119+
}
93120

94-
if (parameters.queries) {
95-
parameters.queries.forEach((query) => {
96-
if (query.data instanceof Error) {
97-
// This is copied from setQueryData() but sets the error.
98-
const cache = queryClient.getQueryCache();
99-
const parsedOptions = parseQueryArgs(query.key)
100-
const defaultedOptions = queryClient.defaultQueryOptions(parsedOptions)
101-
const cachedQuery = cache.build(queryClient, defaultedOptions);
102-
// Set manual data so react-query will not try to refetch.
103-
cachedQuery.setData(undefined, { manual: true });
104-
cachedQuery.setState({ error: query.data });
105-
} else {
106-
queryClient.setQueryData(query.key, query.data);
107-
}
108-
});
109-
}
121+
/** @type {Decorator} */
122+
function withTheme(Story, context) {
123+
const selectedTheme = DecoratorHelpers.pluckThemeFromContext(context);
124+
const { themeOverride } = DecoratorHelpers.useThemeParameters();
125+
const selected = themeOverride || selectedTheme || "dark";
110126

111-
return (
112-
<QueryClientProvider client={queryClient}>
113-
<Story />
114-
</QueryClientProvider>
115-
);
127+
return (
128+
<StrictMode>
129+
<StyledEngineProvider injectFirst>
130+
<MuiThemeProvider theme={themes[selected]}>
131+
<EmotionThemeProvider theme={themes[selected]}>
132+
<CssBaseline />
133+
<Story />
134+
</EmotionThemeProvider>
135+
</MuiThemeProvider>
136+
</StyledEngineProvider>
137+
</StrictMode>
138+
);
116139
}
117140

118141
// Try to fix storybook rendering fonts inconsistently
119142
// https://www.chromatic.com/docs/font-loading/#solution-c-check-fonts-have-loaded-in-a-loader
120143
const fontLoader = async () => ({
121-
fonts: await document.fonts.ready,
144+
fonts: await document.fonts.ready,
122145
});
123146

124147
export const loaders = isChromatic() && document.fonts ? [fontLoader] : [];

0 commit comments

Comments
 (0)