Skip to content

Commit 51ba572

Browse files
authored
[Storybook] Add StateSheet component to include different pseudo-states in the same snapshot (#2550)
## Summary: Builds based on the experiment introduced in #2511 to add a `StateSheet` component to Storybook. This component will be used to test snapshots similar to the Figma Handshake file. The StateSheet component will allow us to create a visual representation of the different states of our components, making it easier to test them while taking less snapshots. This change will also allow us to use this in combination of Chromatic modes in the near future to test combinations like `responsive`, `theming`, `rtl`, etc. Issue: "none" ## Test plan: Verify that the `IconButton - All variants` chromatic snapshot looks similar to the Handshake file. <img width="1354" alt="Screenshot 2025-04-11 at 12 39 37 PM" src="https://github.com/user-attachments/assets/74997401-34d3-4f4e-92e3-d0d492f6500b" /> Author: jandrade Reviewers: beaesguerra, jandrade Required Reviewers: Approved By: beaesguerra Checks: ✅ 10 checks were successful, ⌛ 1 check is pending, ⏭️ 2 checks have been skipped Pull Request URL: #2550
1 parent e33ef43 commit 51ba572

14 files changed

+327
-194
lines changed

.changeset/big-bats-dress.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

__docs__/components/all-variants.tsx

+56-17
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from "react";
22
import type {StrictArgs} from "@storybook/react";
33

44
import {StyleSheet} from "aphrodite";
5-
import {addStyle, View} from "@khanacademy/wonder-blocks-core";
5+
import {addStyle, StyleType, View} from "@khanacademy/wonder-blocks-core";
66
import {
77
border,
88
breakpoint,
@@ -18,14 +18,22 @@ const StyledTh = addStyle("th");
1818
const StyledTd = addStyle("td");
1919
const StyledUl = addStyle("ul");
2020

21-
type Variant = {name: string; props: StrictArgs};
21+
type Variant = {name: string | React.ReactNode; props: StrictArgs};
2222

2323
type Props = {
2424
/**
2525
* The children as a function that receives the state props used to render
2626
* each variant of the component.
2727
*/
28-
children: (props: any, name: string) => React.ReactNode;
28+
children: ({
29+
props,
30+
name,
31+
className,
32+
}: {
33+
props: any;
34+
name?: string;
35+
className?: string;
36+
}) => React.ReactNode;
2937
/**
3038
* The categories to display in the table as columns.
3139
*/
@@ -41,13 +49,35 @@ type Props = {
4149
* - `list`: variants will always be displayed in a list
4250
*/
4351
layout?: "responsive" | "list";
52+
53+
/**
54+
* Custom styles for the `AllVariants` component.
55+
* - `rowHeader`: Styles all the row headers in the table.
56+
* - `cell`: Styles all the cells in the table.
57+
*/
58+
styles?: {
59+
rowHeader?: StyleType;
60+
cell?: StyleType;
61+
};
62+
63+
/**
64+
* The title of the table.
65+
*/
66+
title?: string;
4467
};
4568

4669
/**
4770
* A table that displays all possible variants of a component.
4871
*/
4972
export function AllVariants(props: Props) {
50-
const {children, rows, columns, layout = "responsive"} = props;
73+
const {
74+
children,
75+
rows,
76+
columns,
77+
layout = "responsive",
78+
styles: stylesProp,
79+
title = "Category / State",
80+
} = props;
5181

5282
return (
5383
<>
@@ -56,7 +86,7 @@ export function AllVariants(props: Props) {
5686
<thead>
5787
<tr>
5888
<StyledTh style={styles.cell}>
59-
<LabelLarge>Category / State</LabelLarge>
89+
<LabelLarge>{title}</LabelLarge>
6090
</StyledTh>
6191
{columns.map((col, index) => (
6292
<StyledTh
@@ -72,34 +102,43 @@ export function AllVariants(props: Props) {
72102
<tbody>
73103
{rows.map((row, idx) => (
74104
<tr key={idx}>
75-
<StyledTh scope="row" style={styles.cell}>
76-
<LabelLarge>{row.name}</LabelLarge>
105+
<StyledTh
106+
scope="row"
107+
style={[styles.cell, stylesProp?.rowHeader]}
108+
>
109+
{typeof row.name === "string" ? (
110+
<LabelLarge>{row.name}</LabelLarge>
111+
) : (
112+
row.name
113+
)}
77114
</StyledTh>
78-
{columns.map((col) => (
115+
{columns.map((col, index) => (
79116
<StyledTd
80-
key={col.name}
117+
key={index}
81118
style={[
82119
styles.cell,
83120
{
84121
border: `${border.width.hairline}px dashed ${semanticColor.border.primary}`,
85122
},
123+
stylesProp?.cell,
86124
]}
87125
>
88-
{children(
89-
{
126+
{children({
127+
props: {
90128
"aria-label": `${row.name} ${col.name}`,
91129
...row.props,
92130
...col.props,
93131
},
94-
`${row.name} ${col.name}`,
95-
)}
132+
name: `${row.name} ${col.name}`,
133+
})}
96134
</StyledTd>
97135
))}
98136
</tr>
99137
))}
100138
</tbody>
101139
</StyledTable>
102140
)}
141+
{/* TODO(WB-1925): Make sure to include state labels properly for each children (see `state-sheet.tsx`) */}
103142
<StyledUl
104143
style={[
105144
styles.list,
@@ -115,14 +154,14 @@ export function AllVariants(props: Props) {
115154
</LabelLarge>
116155

117156
<View style={styles.childrenWrapper}>
118-
{children(
119-
{
157+
{children({
158+
props: {
120159
"aria-label": `${row.name} ${column.name}`,
121160
...column.props,
122161
...row.props,
123162
},
124-
`${row.name} ${column.name}`,
125-
)}
163+
name: `${row.name} ${column.name}`,
164+
})}
126165
</View>
127166
</li>
128167
);

__docs__/components/state-sheet.tsx

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import * as React from "react";
2+
3+
import {StyleSheet} from "aphrodite";
4+
import {PropsFor, View} from "@khanacademy/wonder-blocks-core";
5+
import {AllVariants} from "./all-variants";
6+
import {semanticColor, sizing} from "@khanacademy/wonder-blocks-tokens";
7+
import {LabelLarge, LabelMedium} from "@khanacademy/wonder-blocks-typography";
8+
9+
export const commonStates = {
10+
rest: {name: "Rest", className: "rest"},
11+
hover: {name: "Hover", className: "hover"},
12+
press: {name: "Press", className: "press"},
13+
focus: {name: "Focus", className: "focusVisible"},
14+
hoverAndFocus: {name: "Hover + Focus", className: "hover-focusVisible"},
15+
pressAndFocus: {name: "Press + Focus", className: "press-focusVisible"},
16+
};
17+
18+
/**
19+
* The default pseudo states for the `StateSheet` component.
20+
*
21+
* This is used to generate the pseudo states for the component in Chromatic.
22+
* Each className is a CSS class that is applied to the component to simulate
23+
* the pseudo state.
24+
*
25+
* @see https://github.com/chromaui/storybook-addon-pseudo-states?tab=readme-ov-file#targeting-specific-elements
26+
*/
27+
export const defaultPseudoStates = {
28+
hover: [
29+
`.${commonStates.hover.className}`,
30+
`.${commonStates.hoverAndFocus.className}`,
31+
],
32+
focusVisible: [
33+
`.${commonStates.focus.className}`,
34+
`.${commonStates.hoverAndFocus.className}`,
35+
`.${commonStates.pressAndFocus.className}`,
36+
],
37+
active: [
38+
`.${commonStates.press.className}`,
39+
`.${commonStates.pressAndFocus.className}`,
40+
],
41+
};
42+
43+
/**
44+
* The default states that will be included in the `StateSheet` component.
45+
*/
46+
export const defaultStates: Array<State> = Object.values(commonStates);
47+
48+
/**
49+
* A state representing a pseudo state of a component.
50+
* e.g. `rest`, `hover`, `press`, `focus`, etc.
51+
*/
52+
type State = {name: string; className: string};
53+
54+
/**
55+
* The labels for the states presented in the row headers in the `StateSheet`
56+
* component.
57+
*/
58+
function StateLabels({
59+
states,
60+
variant,
61+
}: {
62+
states: Array<State>;
63+
variant: string | React.ReactNode;
64+
}) {
65+
return (
66+
<View style={styles.stateLabelsContainer}>
67+
<LabelLarge style={{alignSelf: "center"}}>{variant}</LabelLarge>
68+
<View
69+
style={[
70+
styles.rowHeaderStates,
71+
{gridTemplateRows: `repeat(${states.length}, 40px)`},
72+
]}
73+
>
74+
{states.map(({name}, index) => (
75+
<LabelMedium key={index}>{name}</LabelMedium>
76+
))}
77+
</View>
78+
</View>
79+
);
80+
}
81+
82+
type Props = PropsFor<typeof AllVariants> & {
83+
/**
84+
* The states to display in the table as rows.
85+
*/
86+
states?: Array<State>;
87+
};
88+
89+
/**
90+
* A table that displays all possible states of a component.
91+
*/
92+
export function StateSheet({
93+
children,
94+
columns,
95+
rows,
96+
states = defaultStates,
97+
title,
98+
}: Props) {
99+
// Override the default row headers to include the state labels.
100+
const rowsWithStateLabels = rows.map(({name, props}) => ({
101+
name: <StateLabels variant={name} states={states} />,
102+
props,
103+
}));
104+
105+
return (
106+
<View>
107+
<AllVariants
108+
rows={rowsWithStateLabels}
109+
columns={columns}
110+
styles={{
111+
rowHeader: styles.rowHeader,
112+
}}
113+
title={title}
114+
>
115+
{({props}) => {
116+
return (
117+
<View style={styles.container}>
118+
{states.map(({className, name}) =>
119+
children({
120+
props,
121+
className,
122+
name,
123+
}),
124+
)}
125+
</View>
126+
);
127+
}}
128+
</AllVariants>
129+
</View>
130+
);
131+
}
132+
133+
const styles = StyleSheet.create({
134+
rowHeader: {
135+
verticalAlign: "top",
136+
padding: 0,
137+
},
138+
container: {
139+
gap: sizing.size_160,
140+
},
141+
142+
stateLabelsContainer: {
143+
flexDirection: "row",
144+
gap: sizing.size_160,
145+
padding: sizing.size_160,
146+
justifyContent: "space-between",
147+
background: semanticColor.surface.secondary,
148+
borderTop: `${sizing.size_010} solid ${semanticColor.border.strong}`,
149+
},
150+
rowHeaderStates: {
151+
gap: sizing.size_160,
152+
display: "grid",
153+
alignItems: "center",
154+
textAlign: "right",
155+
},
156+
});

__docs__/wonder-blocks-dropdown/action-item-variants.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const meta = {
4848
component: ActionItem,
4949
render: (args) => (
5050
<AllVariants rows={rows} columns={columns}>
51-
{(props) => <ActionItem {...args} {...props} />}
51+
{({props}) => <ActionItem {...args} {...props} />}
5252
</AllVariants>
5353
),
5454
args: {

__docs__/wonder-blocks-dropdown/option-item-variants.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const meta = {
5151
component: OptionItem,
5252
render: (args) => (
5353
<AllVariants rows={rows} columns={columns}>
54-
{(props) => (
54+
{({props}) => (
5555
<div aria-label={props.ariaLabel} role="listbox">
5656
<OptionItem {...args} {...props} />
5757
</div>

__docs__/wonder-blocks-form/checkbox-variants.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const meta = {
3737
component: Checkbox,
3838
render: (args) => (
3939
<AllVariants rows={rows} columns={columns}>
40-
{(props) => <Checkbox {...args} {...props} />}
40+
{({props}) => <Checkbox {...args} {...props} />}
4141
</AllVariants>
4242
),
4343
args: {

__docs__/wonder-blocks-form/radio-variants.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const meta = {
3838
component: Radio,
3939
render: (args) => (
4040
<AllVariants rows={rows} columns={columns}>
41-
{(props) => <Radio {...args} {...props} />}
41+
{({props}) => <Radio {...args} {...props} />}
4242
</AllVariants>
4343
),
4444
args: {

0 commit comments

Comments
 (0)