From 2e277e1013ff53dfa491b0b480fb800a745a9dbb Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Wed, 22 Jan 2025 10:06:26 -0500 Subject: [PATCH 01/11] [form-cb-rb-semantic-v2] Generate All Variant stories for Checkbox and Radio components --- __docs__/components/all-variants.tsx | 92 +++++++++++++++++++ .../checkbox-variants.stories.tsx | 69 ++++++++++++++ .../wonder-blocks-form/checkbox.stories.tsx | 22 +---- .../radio-variants.stories.tsx | 70 ++++++++++++++ __docs__/wonder-blocks-form/radio.stories.tsx | 17 +--- 5 files changed, 239 insertions(+), 31 deletions(-) create mode 100644 __docs__/components/all-variants.tsx create mode 100644 __docs__/wonder-blocks-form/checkbox-variants.stories.tsx create mode 100644 __docs__/wonder-blocks-form/radio-variants.stories.tsx diff --git a/__docs__/components/all-variants.tsx b/__docs__/components/all-variants.tsx new file mode 100644 index 000000000..6e5e997ba --- /dev/null +++ b/__docs__/components/all-variants.tsx @@ -0,0 +1,92 @@ +import * as React from "react"; +import type {StrictArgs} from "@storybook/react"; + +import {StyleSheet} from "aphrodite"; +import {addStyle} from "@khanacademy/wonder-blocks-core"; +import { + border, + semanticColor, + spacing, +} from "@khanacademy/wonder-blocks-tokens"; + +import {LabelLarge} from "@khanacademy/wonder-blocks-typography"; + +const StyledTable = addStyle("table"); +const StyledTh = addStyle("th"); +const StyledTd = addStyle("td"); + +type Variant = {name: string; props: StrictArgs}; + +type Props = { + /** + * The children as a function that receives the state props used to render + * each variant of the component. + */ + children: (props: any) => React.ReactNode; + /** + * The categories to display in the table as columns. + */ + rows: Array; + /** + * The states to display in the table as rows. + */ + columns: Array; +}; + +/** + * A table that displays all possible variants of a component. + */ +export function AllVariants({children, columns, rows}: Props) { + return ( + + + + + Category / State + + {columns.map((col, index) => ( + + {col.name} + + ))} + + + + {rows.map((row, idx) => ( + + + {row.name} + + {columns.map((col) => ( + + {children({ + ...row.props, + ...col.props, + })} + + ))} + + ))} + + + ); +} + +const styles = StyleSheet.create({ + table: { + borderCollapse: "collapse", + textAlign: "left", + }, + cell: { + margin: spacing.medium_16, + padding: spacing.medium_16, + }, +}); diff --git a/__docs__/wonder-blocks-form/checkbox-variants.stories.tsx b/__docs__/wonder-blocks-form/checkbox-variants.stories.tsx new file mode 100644 index 000000000..f182fa8d8 --- /dev/null +++ b/__docs__/wonder-blocks-form/checkbox-variants.stories.tsx @@ -0,0 +1,69 @@ +import * as React from "react"; +import type {Meta, StoryObj} from "@storybook/react"; + +import {Checkbox} from "@khanacademy/wonder-blocks-form"; + +import {AllVariants} from "../components/all-variants"; + +const rows = [ + {name: "Unchecked", props: {checked: false}}, + {name: "Checked", props: {checked: true}}, + {name: "Indeterminate", props: {checked: null}}, +]; + +const columns = [ + { + name: "Default", + props: {}, + }, + { + name: "Disabled", + props: {disabled: true}, + }, + { + name: "Error", + props: {error: true}, + }, +]; + +type Story = StoryObj; + +/** + * The following stories are used to generate the pseudo states for the Checkbox + * component. This is only used for visual testing in Chromatic. + */ +const meta = { + title: "Packages / Form / Checkbox / Checkbox - All Variants", + component: Checkbox, + render: (args) => ( + + {(props) => } + + ), + args: { + label: "Label", + description: "Description", + }, + tags: ["!autodocs"], +} satisfies Meta; + +export default meta; + +export const Default: Story = {}; + +export const Hover: Story = { + parameters: {pseudo: {hover: true}}, +}; + +export const Focus: Story = { + parameters: {pseudo: {focusVisible: true}}, +}; + +export const HoverFocus: Story = { + name: "Hover + Focus", + parameters: {pseudo: {hover: true, focusVisible: true}}, +}; + +export const Active: Story = { + parameters: {pseudo: {active: true}}, +}; diff --git a/__docs__/wonder-blocks-form/checkbox.stories.tsx b/__docs__/wonder-blocks-form/checkbox.stories.tsx index cd2e6b65d..985068fb2 100644 --- a/__docs__/wonder-blocks-form/checkbox.stories.tsx +++ b/__docs__/wonder-blocks-form/checkbox.stories.tsx @@ -21,6 +21,10 @@ export default { version={packageConfig.version} /> ), + chromatic: { + // These stories are being tested in checkbox-variants.stories.tsx + disableSnapshot: true, + }, }, } as Meta; @@ -33,14 +37,6 @@ export const Default: StoryComponentType = { }, }; -Default.parameters = { - chromatic: { - // We already have screenshots of another story that covers - // this and more cases. - disableSnapshot: true, - }, -}; - export const Controlled: StoryComponentType = () => { const [checked, setChecked] = React.useState(null); @@ -55,11 +51,6 @@ export const Controlled: StoryComponentType = () => { }; Controlled.parameters = { - chromatic: { - // Disabling because this doesn't test visuals, its for testing - // that `state` works as expected. - disableSnapshot: true, - }, docs: { description: { story: `Use state to keep track of whether the checkbox @@ -258,11 +249,6 @@ export const VariantsControlled: StoryComponentType = () => { }; VariantsControlled.parameters = { - chromatic: { - // Disabling because this doesn't test visuals, its for testing - // that `state` works as expected. - disableSnapshot: true, - }, docs: { description: { story: `A demo of the different kinds of checkboxes diff --git a/__docs__/wonder-blocks-form/radio-variants.stories.tsx b/__docs__/wonder-blocks-form/radio-variants.stories.tsx new file mode 100644 index 000000000..7f625bb04 --- /dev/null +++ b/__docs__/wonder-blocks-form/radio-variants.stories.tsx @@ -0,0 +1,70 @@ +import * as React from "react"; +import type {Meta, StoryObj} from "@storybook/react"; + +// NOTE: Radio is an internal component and should not be used directly. Use +// RadioGroup instead. This import is only used for visual testing in Chromatic. +import Radio from "../../packages/wonder-blocks-form/src/components/radio"; + +import {AllVariants} from "../components/all-variants"; + +const rows = [ + {name: "Unchecked", props: {checked: false}}, + {name: "Checked", props: {checked: true}}, +]; + +const columns = [ + { + name: "Default", + props: {}, + }, + { + name: "Disabled", + props: {disabled: true}, + }, + { + name: "Error", + props: {error: true}, + }, +]; + +type Story = StoryObj; + +/** + * The following stories are used to generate the pseudo states for the Radio + * component. This is only used for visual testing in Chromatic. + */ +const meta = { + title: "Packages / Form / Radio (internal) / Radio - All Variants", + component: Radio, + render: (args) => ( + + {(props) => } + + ), + args: { + label: "Label", + description: "Description", + }, + tags: ["!autodocs"], +} satisfies Meta; + +export default meta; + +export const Default: Story = {}; + +export const Hover: Story = { + parameters: {pseudo: {hover: true}}, +}; + +export const Focus: Story = { + parameters: {pseudo: {focusVisible: true}}, +}; + +export const HoverFocus: Story = { + name: "Hover + Focus", + parameters: {pseudo: {hover: true, focusVisible: true}}, +}; + +export const Active: Story = { + parameters: {pseudo: {active: true}}, +}; diff --git a/__docs__/wonder-blocks-form/radio.stories.tsx b/__docs__/wonder-blocks-form/radio.stories.tsx index 846597928..7b6c4b305 100644 --- a/__docs__/wonder-blocks-form/radio.stories.tsx +++ b/__docs__/wonder-blocks-form/radio.stories.tsx @@ -22,6 +22,10 @@ export default { version={packageConfig.version} /> ), + chromatic: { + // These stories are being tested in radio-variants.stories.tsx + disableSnapshot: true, + }, }, } as Meta; @@ -32,25 +36,12 @@ export const Default: StoryComponentType = { }, }; -Default.parameters = { - chromatic: { - // We already have screenshots of another story that covers - // this and more cases. - disableSnapshot: true, - }, -}; - export const Controlled: StoryComponentType = () => { const [checked, setChecked] = React.useState(false); return ; }; Controlled.parameters = { - chromatic: { - // Disabling because this doesn't test visuals, it tests - // that the `checked` state works as expected. - disableSnapshot: true, - }, docs: { description: { story: `Use state to keep track of whether From dd91fb7dfe3df3557382175a2882e3cc9ef152b5 Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Wed, 22 Jan 2025 11:24:53 -0500 Subject: [PATCH 02/11] [form-cb-rb-semantic-v2] Migrate Radio and Checkbox to use semanticColor tokens --- .../src/components/checkbox-core.tsx | 95 ++++++++++--------- .../src/components/choice-internal.tsx | 6 +- .../src/components/group-styles.ts | 6 +- .../src/components/radio-core.tsx | 80 ++++++++-------- 4 files changed, 96 insertions(+), 91 deletions(-) diff --git a/packages/wonder-blocks-form/src/components/checkbox-core.tsx b/packages/wonder-blocks-form/src/components/checkbox-core.tsx index a063b354b..1d8c0d6b6 100644 --- a/packages/wonder-blocks-form/src/components/checkbox-core.tsx +++ b/packages/wonder-blocks-form/src/components/checkbox-core.tsx @@ -1,7 +1,11 @@ import * as React from "react"; import {StyleSheet} from "aphrodite"; -import {mix, color, spacing} from "@khanacademy/wonder-blocks-tokens"; +import { + border, + spacing, + semanticColor, +} from "@khanacademy/wonder-blocks-tokens"; import {addStyle} from "@khanacademy/wonder-blocks-core"; import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon"; import checkIcon from "@phosphor-icons/core/bold/check-bold.svg"; @@ -25,8 +29,6 @@ function mapCheckedToAriaChecked(value: Checked): AriaChecked { } } -const {blue, red, white, offWhite, offBlack16, offBlack32, offBlack50} = color; - // The checkbox size const size = spacing.medium_16; // The check icon size @@ -69,7 +71,11 @@ const CheckboxCore = React.forwardRef(function CheckboxCore( const checkboxIcon = ( = {}; const _generateStyles = (checked: Checked, error: boolean) => { @@ -179,55 +167,72 @@ const _generateStyles = (checked: Checked, error: boolean) => { return styles[styleKey]; } - const palette = error ? colors.error : colors.default; + const isCheckedOrIndeterminate = checked || checked == null; + const actionType = error ? "destructive" : "progressive"; + const styleType = isCheckedOrIndeterminate ? "filled" : "outlined"; + + const colorAction = semanticColor.action[styleType][actionType]; let newStyles: Record = {}; - if (checked || checked == null) { + + if (isCheckedOrIndeterminate) { newStyles = { default: { - backgroundColor: palette.base, - borderWidth: 0, + backgroundColor: colorAction.default.background, + borderColor: colorAction.default.border, // Focus and hover have the same style. Focus style only shows // up with keyboard navigation. ":focus-visible": { - boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`, + outline: `${border.width.thin}px solid ${colorAction.hover.border}`, + outlineOffset: 1, }, ":hover": { - boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`, + outline: `${border.width.thin}px solid ${colorAction.hover.border}`, + outlineOffset: 1, }, ":active": { - boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.active}`, - background: palette.active, + outline: `${border.width.thin}px solid ${colorAction.press.border}`, + outlineOffset: 1, + background: colorAction.press.background, }, }, }; + // Unchecked state } else { newStyles = { default: { - backgroundColor: error ? fadedRed : white, - borderColor: error ? red : offBlack50, + backgroundColor: error + ? semanticColor.status.critical.background + : colorAction.default.background, + borderColor: error + ? semanticColor.status.critical.foreground + : colorAction.default.border, // Focus and hover have the same style. Focus style only shows // up with keyboard navigation. ":focus-visible": { - backgroundColor: error ? fadedRed : white, - borderColor: palette.base, - borderWidth: 2, + backgroundColor: error + ? semanticColor.status.critical.background + : colorAction.hover.background, + outline: `${border.width.thin}px solid ${colorAction.hover.border}`, + outlineOffset: -1, }, ":hover": { - backgroundColor: error ? fadedRed : white, - borderColor: palette.base, - borderWidth: 2, + backgroundColor: error + ? semanticColor.status.critical.background + : colorAction.hover.background, + outline: `${border.width.thin}px solid ${colorAction.hover.border}`, + outlineOffset: -1, }, ":active": { - backgroundColor: palette.faded, - borderColor: error ? activeRed : blue, - borderWidth: 2, + backgroundColor: colorAction.press.background, + outline: `${border.width.thin}px solid ${colorAction.press.border}`, + outlineOffset: -1, }, }, }; diff --git a/packages/wonder-blocks-form/src/components/choice-internal.tsx b/packages/wonder-blocks-form/src/components/choice-internal.tsx index 4d0efc717..0b55685cd 100644 --- a/packages/wonder-blocks-form/src/components/choice-internal.tsx +++ b/packages/wonder-blocks-form/src/components/choice-internal.tsx @@ -3,7 +3,7 @@ import {StyleSheet} from "aphrodite"; import {View, Id} from "@khanacademy/wonder-blocks-core"; import {Strut} from "@khanacademy/wonder-blocks-layout"; -import {color, spacing} from "@khanacademy/wonder-blocks-tokens"; +import {semanticColor, spacing} from "@khanacademy/wonder-blocks-tokens"; import {LabelMedium, LabelSmall} from "@khanacademy/wonder-blocks-typography"; import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core"; import CheckboxCore from "./checkbox-core"; @@ -169,13 +169,13 @@ const styles = StyleSheet.create({ marginTop: -2, }, disabledLabel: { - color: color.offBlack32, + color: semanticColor.text.disabled, }, description: { // 16 for icon + 8 for spacing strut marginLeft: spacing.medium_16 + spacing.xSmall_8, marginTop: spacing.xxxSmall_4, - color: color.offBlack64, + color: semanticColor.text.secondary, }, }); diff --git a/packages/wonder-blocks-form/src/components/group-styles.ts b/packages/wonder-blocks-form/src/components/group-styles.ts index 7089dfe6b..f5be383fd 100644 --- a/packages/wonder-blocks-form/src/components/group-styles.ts +++ b/packages/wonder-blocks-form/src/components/group-styles.ts @@ -1,6 +1,6 @@ import {StyleSheet} from "aphrodite"; -import {color, spacing} from "@khanacademy/wonder-blocks-tokens"; +import {semanticColor, spacing} from "@khanacademy/wonder-blocks-tokens"; import type {StyleDeclaration} from "aphrodite"; @@ -22,12 +22,12 @@ const styles: StyleDeclaration = StyleSheet.create({ description: { marginTop: spacing.xxxSmall_4, - color: color.offBlack64, + color: semanticColor.text.secondary, }, error: { marginTop: spacing.xxxSmall_4, - color: color.red, + color: semanticColor.status.critical.foreground, }, defaultLineGap: { diff --git a/packages/wonder-blocks-form/src/components/radio-core.tsx b/packages/wonder-blocks-form/src/components/radio-core.tsx index f43206295..72f0ca61f 100644 --- a/packages/wonder-blocks-form/src/components/radio-core.tsx +++ b/packages/wonder-blocks-form/src/components/radio-core.tsx @@ -1,13 +1,11 @@ import * as React from "react"; import {StyleSheet} from "aphrodite"; -import {mix, color} from "@khanacademy/wonder-blocks-tokens"; +import {border, semanticColor} from "@khanacademy/wonder-blocks-tokens"; import {addStyle} from "@khanacademy/wonder-blocks-core"; import type {ChoiceCoreProps, Checked} from "../util/types"; -const {blue, red, white, offWhite, offBlack16, offBlack32, offBlack50} = color; - const StyledInput = addStyle("input"); /** @@ -62,7 +60,7 @@ const disabledChecked = { height: size / 2, width: size / 2, borderRadius: "50%", - backgroundColor: offBlack32, + backgroundColor: semanticColor.action.disabled.secondary, } as const; const sharedStyles = StyleSheet.create({ // Reset the default styled input element @@ -80,30 +78,17 @@ const sharedStyles = StyleSheet.create({ outline: "none", boxSizing: "border-box", borderStyle: "solid", - borderWidth: 1, + borderWidth: border.width.hairline, borderRadius: "50%", }, disabled: { cursor: "auto", - backgroundColor: offWhite, - borderColor: offBlack16, - borderWidth: 1, + backgroundColor: semanticColor.action.disabled.secondary, + borderColor: semanticColor.border.primary, + borderWidth: border.width.hairline, }, }); -const fadedBlue = mix(color.fadedBlue16, white); -const fadedRed = mix(color.fadedRed8, white); -const colors = { - default: { - faded: fadedBlue, - base: blue, - active: color.activeBlue, - }, - error: { - faded: fadedRed, - base: red, - active: color.activeRed, - }, -} as const; + const styles: Record = {}; const _generateStyles = (checked: Checked, error: boolean) => { // "hash" the parameters @@ -111,55 +96,70 @@ const _generateStyles = (checked: Checked, error: boolean) => { if (styles[styleKey]) { return styles[styleKey]; } - const palette = error ? colors.error : colors.default; + const actionType = error ? "destructive" : "progressive"; + // NOTE: Radio buttons use the outlined style regardless of the checked + // state. + const colorAction = semanticColor.action.outlined[actionType]; + let newStyles: Record = {}; if (checked) { newStyles = { default: { - backgroundColor: white, - borderColor: palette.base, + backgroundColor: colorAction.default.background, + borderColor: colorAction.default.foreground, borderWidth: size / 4, // Focus and hover have the same style. Focus style only shows // up with keyboard navigation. ":focus-visible": { - boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`, + outline: `${border.width.thin}px solid ${colorAction.hover.border}`, + outlineOffset: 1, }, ":hover": { - boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`, + outline: `${border.width.thin}px solid ${colorAction.hover.border}`, + outlineOffset: 1, }, ":active": { - boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.active}`, - borderColor: palette.active, + outline: `${border.width.thin}px solid ${colorAction.press.border}`, + outlineOffset: 1, + borderColor: colorAction.press.border, }, }, }; } else { newStyles = { default: { - backgroundColor: error ? fadedRed : white, - borderColor: error ? red : offBlack50, + backgroundColor: error + ? semanticColor.status.critical.background + : colorAction.default.background, + borderColor: error + ? semanticColor.status.critical.foreground + : colorAction.default.border, // Focus and hover have the same style. Focus style only shows // up with keyboard navigation. ":focus-visible": { - backgroundColor: error ? fadedRed : white, - borderColor: palette.base, - borderWidth: 2, + backgroundColor: error + ? semanticColor.status.critical.background + : colorAction.hover.background, + outline: `${border.width.thin}px solid ${colorAction.hover.border}`, + outlineOffset: -1, }, ":hover": { - backgroundColor: error ? fadedRed : white, - borderColor: palette.base, - borderWidth: 2, + backgroundColor: error + ? semanticColor.status.critical.background + : colorAction.hover.background, + outline: `${border.width.thin}px solid ${colorAction.hover.border}`, + outlineOffset: -1, }, ":active": { - backgroundColor: palette.faded, - borderColor: error ? color.activeRed : blue, - borderWidth: 2, + backgroundColor: colorAction.press.background, + outline: `${border.width.thin}px solid ${colorAction.press.border}`, + outlineOffset: -1, }, }, }; From 4200fa1c386dbdfc7bcbad56dac7fad02a44fe66 Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Wed, 22 Jan 2025 11:25:10 -0500 Subject: [PATCH 03/11] [form-cb-rb-semantic-v2] docs(changeset): Migrate Radio and Checkbox to use semanticColor tokens --- .changeset/unlucky-planes-admire.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/unlucky-planes-admire.md diff --git a/.changeset/unlucky-planes-admire.md b/.changeset/unlucky-planes-admire.md new file mode 100644 index 000000000..49dd4e2f4 --- /dev/null +++ b/.changeset/unlucky-planes-admire.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/wonder-blocks-form": patch +--- + +Migrate Radio and Checkbox to use semanticColor tokens From b5a72c9a294b60cfa6f5cef6753f9805d421b912 Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Wed, 22 Jan 2025 11:30:17 -0500 Subject: [PATCH 04/11] [form-cb-rb-semantic-v2] Remove redundant tests in favor of chromatic snapshots --- .../custom-snapshot.test.tsx.snap | 247 ------------------ .../src/__tests__/custom-snapshot.test.tsx | 48 ---- 2 files changed, 295 deletions(-) delete mode 100644 packages/wonder-blocks-form/src/__tests__/__snapshots__/custom-snapshot.test.tsx.snap delete mode 100644 packages/wonder-blocks-form/src/__tests__/custom-snapshot.test.tsx diff --git a/packages/wonder-blocks-form/src/__tests__/__snapshots__/custom-snapshot.test.tsx.snap b/packages/wonder-blocks-form/src/__tests__/__snapshots__/custom-snapshot.test.tsx.snap deleted file mode 100644 index 684418e85..000000000 --- a/packages/wonder-blocks-form/src/__tests__/__snapshots__/custom-snapshot.test.tsx.snap +++ /dev/null @@ -1,247 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CheckboxCore type:default checked:false 1`] = ` -
- -
-`; - -exports[`CheckboxCore type:default checked:null 1`] = ` -
- - -
-`; - -exports[`CheckboxCore type:default checked:true 1`] = ` -
- - -
-`; - -exports[`CheckboxCore type:disabled checked:false 1`] = ` -
- -
-`; - -exports[`CheckboxCore type:disabled checked:null 1`] = ` -
- - -
-`; - -exports[`CheckboxCore type:disabled checked:true 1`] = ` -
- - -
-`; - -exports[`CheckboxCore type:error checked:false 1`] = ` -
- -
-`; - -exports[`CheckboxCore type:error checked:null 1`] = ` -
- - -
-`; - -exports[`CheckboxCore type:error checked:true 1`] = ` -
- - -
-`; - -exports[`RadioCore type:default checked:false 1`] = ` -
- -
-`; - -exports[`RadioCore type:default checked:null 1`] = ` -
- -
-`; - -exports[`RadioCore type:default checked:true 1`] = ` -
- -
-`; - -exports[`RadioCore type:disabled checked:false 1`] = ` -
- -
-`; - -exports[`RadioCore type:disabled checked:null 1`] = ` -
- -
-`; - -exports[`RadioCore type:disabled checked:true 1`] = ` -
- - -
-`; - -exports[`RadioCore type:error checked:false 1`] = ` -
- -
-`; - -exports[`RadioCore type:error checked:null 1`] = ` -
- -
-`; - -exports[`RadioCore type:error checked:true 1`] = ` -
- -
-`; diff --git a/packages/wonder-blocks-form/src/__tests__/custom-snapshot.test.tsx b/packages/wonder-blocks-form/src/__tests__/custom-snapshot.test.tsx deleted file mode 100644 index 4c33aa4f6..000000000 --- a/packages/wonder-blocks-form/src/__tests__/custom-snapshot.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from "react"; -import {render} from "@testing-library/react"; - -import CheckboxCore from "../components/checkbox-core"; -import RadioCore from "../components/radio-core"; - -const states = ["default", "error", "disabled"]; -const checkedStates = [false, true, null]; - -describe("CheckboxCore", () => { - states.forEach((state: any) => { - checkedStates.forEach((checked: any) => { - test(`type:${state} checked:${String(checked)}`, () => { - const disabled = state === "disabled"; - const {container} = render( - {}} - />, - ); - - expect(container).toMatchSnapshot(); - }); - }); - }); -}); - -describe("RadioCore", () => { - states.forEach((state: any) => { - checkedStates.forEach((checked: any) => { - test(`type:${state} checked:${String(checked)}`, () => { - const disabled = state === "disabled"; - const {container} = render( - {}} - />, - ); - - expect(container).toMatchSnapshot(); - }); - }); - }); -}); From a933be0d845a3efc4c64ee82fce0378fab51f5c1 Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Wed, 22 Jan 2025 11:34:41 -0500 Subject: [PATCH 05/11] [form-cb-rb-semantic-v2] Fix disabled styles --- packages/wonder-blocks-form/src/components/radio-core.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wonder-blocks-form/src/components/radio-core.tsx b/packages/wonder-blocks-form/src/components/radio-core.tsx index 72f0ca61f..ea555e21f 100644 --- a/packages/wonder-blocks-form/src/components/radio-core.tsx +++ b/packages/wonder-blocks-form/src/components/radio-core.tsx @@ -60,7 +60,7 @@ const disabledChecked = { height: size / 2, width: size / 2, borderRadius: "50%", - backgroundColor: semanticColor.action.disabled.secondary, + backgroundColor: semanticColor.action.disabled.default, } as const; const sharedStyles = StyleSheet.create({ // Reset the default styled input element From 714e51ae99024154eee1e895377209be04b5e085 Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Wed, 22 Jan 2025 11:52:21 -0500 Subject: [PATCH 06/11] [form-cb-rb-semantic-v2] Use semanticColor in stories --- .../checkbox-accessibility.stories.tsx | 9 +++++---- __docs__/wonder-blocks-form/checkbox-group.stories.tsx | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/__docs__/wonder-blocks-form/checkbox-accessibility.stories.tsx b/__docs__/wonder-blocks-form/checkbox-accessibility.stories.tsx index aec990a0d..b2db48be2 100644 --- a/__docs__/wonder-blocks-form/checkbox-accessibility.stories.tsx +++ b/__docs__/wonder-blocks-form/checkbox-accessibility.stories.tsx @@ -4,25 +4,26 @@ import {StyleSheet} from "aphrodite"; import {PropsFor, View} from "@khanacademy/wonder-blocks-core"; import {Checkbox} from "@khanacademy/wonder-blocks-form"; import {LabelSmall} from "@khanacademy/wonder-blocks-typography"; -import {color} from "@khanacademy/wonder-blocks-tokens"; +import {semanticColor} from "@khanacademy/wonder-blocks-tokens"; type CheckboxProps = PropsFor; const ErrorTemplate = (args: CheckboxProps) => { const [checked, setChecked] = React.useState(false); + const errorId = React.useId(); const errorState = !checked; return ( {errorState && ( - + You must agree to the terms to continue )} @@ -45,7 +46,7 @@ const DisabledTemplate = (args: CheckboxProps) => { const styles = StyleSheet.create({ error: { - color: color.red, + color: semanticColor.status.critical.foreground, }, }); diff --git a/__docs__/wonder-blocks-form/checkbox-group.stories.tsx b/__docs__/wonder-blocks-form/checkbox-group.stories.tsx index 8124b2029..4472accb4 100644 --- a/__docs__/wonder-blocks-form/checkbox-group.stories.tsx +++ b/__docs__/wonder-blocks-form/checkbox-group.stories.tsx @@ -3,7 +3,7 @@ import {StyleSheet} from "aphrodite"; import type {Meta, StoryObj} from "@storybook/react"; import {View} from "@khanacademy/wonder-blocks-core"; -import {color, spacing} from "@khanacademy/wonder-blocks-tokens"; +import {semanticColor, spacing} from "@khanacademy/wonder-blocks-tokens"; import {LabelLarge, LabelXSmall} from "@khanacademy/wonder-blocks-typography"; import {Choice, CheckboxGroup} from "@khanacademy/wonder-blocks-form"; @@ -329,7 +329,7 @@ const styles = StyleSheet.create({ }, title: { paddingBottom: spacing.xSmall_8, - borderBottom: `1px solid ${color.offBlack64}`, + borderBottom: `1px solid ${semanticColor.border.strong}`, }, // Multiple choice styling multipleChoice: { @@ -339,7 +339,7 @@ const styles = StyleSheet.create({ justifyContent: "center", }, description: { - color: color.offBlack64, + color: semanticColor.text.secondary, }, last: { borderBottom: "solid 1px #CCC", From 6846854a93795355e78a3d951c210d811592ce8e Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Tue, 28 Jan 2025 12:48:48 -0500 Subject: [PATCH 07/11] [form-cb-rb-semantic-v2] docs(changeset): Add `icon.disabled` token to semanticColor. --- .changeset/eighty-zebras-burn.md | 5 +++++ packages/wonder-blocks-tokens/src/tokens/semantic-color.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/eighty-zebras-burn.md diff --git a/.changeset/eighty-zebras-burn.md b/.changeset/eighty-zebras-burn.md new file mode 100644 index 000000000..405b52aa2 --- /dev/null +++ b/.changeset/eighty-zebras-burn.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/wonder-blocks-tokens": minor +--- + +Add `icon.disabled` token to semanticColor. diff --git a/packages/wonder-blocks-tokens/src/tokens/semantic-color.ts b/packages/wonder-blocks-tokens/src/tokens/semantic-color.ts index 367dc4830..d8404199a 100644 --- a/packages/wonder-blocks-tokens/src/tokens/semantic-color.ts +++ b/packages/wonder-blocks-tokens/src/tokens/semantic-color.ts @@ -156,6 +156,7 @@ export const semanticColor = { inverse: color.white, action: color.blue, destructive: color.red, + disabled: color.fadedOffBlack32, }, /** * Colors to be used exclusively for Khanmigo or to communicate a From 8a92bc0d31de98206c35cc9128a879643aa2ed94 Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Tue, 28 Jan 2025 15:24:40 -0500 Subject: [PATCH 08/11] [form-cb-rb-semantic-v2] Address feedback --- .../src/components/checkbox-core.tsx | 47 +++++++++++++------ .../src/components/radio-core.tsx | 47 ++++++++++++++----- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/packages/wonder-blocks-form/src/components/checkbox-core.tsx b/packages/wonder-blocks-form/src/components/checkbox-core.tsx index 1d8c0d6b6..8029ae284 100644 --- a/packages/wonder-blocks-form/src/components/checkbox-core.tsx +++ b/packages/wonder-blocks-form/src/components/checkbox-core.tsx @@ -73,7 +73,7 @@ const CheckboxCore = React.forwardRef(function CheckboxCore( { const colorAction = semanticColor.action[styleType][actionType]; + // The different states that the component can be in. + const states = { + // Resting state + default: { + border: colorAction.default.border, + background: colorAction.default.background, + }, + // Form validation error state + error: { + border: semanticColor.status.critical.foreground, + background: semanticColor.status.critical.background, + }, + }; + let newStyles: Record = {}; if (isCheckedOrIndeterminate) { newStyles = { default: { - backgroundColor: colorAction.default.background, - borderColor: colorAction.default.border, + backgroundColor: states.default.background, + borderColor: states.default.border, // Focus and hover have the same style. Focus style only shows // up with keyboard navigation. ":focus-visible": { + // TODO(WB-1856): Use the correct border color for focus outline: `${border.width.thin}px solid ${colorAction.hover.border}`, outlineOffset: 1, }, @@ -202,28 +222,27 @@ const _generateStyles = (checked: Checked, error: boolean) => { }; // Unchecked state } else { + const currentState = error ? states.error : states.default; + newStyles = { default: { - backgroundColor: error - ? semanticColor.status.critical.background - : colorAction.default.background, - borderColor: error - ? semanticColor.status.critical.foreground - : colorAction.default.border, + backgroundColor: currentState.background, + borderColor: currentState.border, // Focus and hover have the same style. Focus style only shows // up with keyboard navigation. ":focus-visible": { backgroundColor: error - ? semanticColor.status.critical.background + ? states.error.background : colorAction.hover.background, + // TODO(WB-1856): Use the correct border color for focus outline: `${border.width.thin}px solid ${colorAction.hover.border}`, outlineOffset: -1, }, ":hover": { backgroundColor: error - ? semanticColor.status.critical.background + ? states.error.background : colorAction.hover.background, outline: `${border.width.thin}px solid ${colorAction.hover.border}`, outlineOffset: -1, diff --git a/packages/wonder-blocks-form/src/components/radio-core.tsx b/packages/wonder-blocks-form/src/components/radio-core.tsx index ea555e21f..0df5909f2 100644 --- a/packages/wonder-blocks-form/src/components/radio-core.tsx +++ b/packages/wonder-blocks-form/src/components/radio-core.tsx @@ -52,6 +52,11 @@ const StyledInput = addStyle("input"); ); }); +const disabledState = { + border: semanticColor.border.primary, + background: semanticColor.action.disabled.secondary, +}; + const size = 16; // circle with a different color. Here, we add that center circle. // If the checkbox is disabled and selected, it has a border but also an inner const disabledChecked = { position: "absolute", @@ -62,6 +67,7 @@ const disabledChecked = { borderRadius: "50%", backgroundColor: semanticColor.action.disabled.default, } as const; + const sharedStyles = StyleSheet.create({ // Reset the default styled input element inputReset: { @@ -83,8 +89,8 @@ const sharedStyles = StyleSheet.create({ }, disabled: { cursor: "auto", - backgroundColor: semanticColor.action.disabled.secondary, - borderColor: semanticColor.border.primary, + backgroundColor: disabledState.background, + borderColor: disabledState.border, borderWidth: border.width.hairline, }, }); @@ -101,17 +107,35 @@ const _generateStyles = (checked: Checked, error: boolean) => { // state. const colorAction = semanticColor.action.outlined[actionType]; + // The different states that the component can be in. + const states = { + // Resting state + default: { + // NOTE: This is a special case where the border is the same color + // as the foreground. This should change as soon as we simplify the + // existing `action` tokens. + border: colorAction.default.foreground, + background: colorAction.default.background, + }, + // Form validation error state + error: { + border: semanticColor.status.critical.foreground, + background: semanticColor.status.critical.background, + }, + }; + let newStyles: Record = {}; if (checked) { newStyles = { default: { - backgroundColor: colorAction.default.background, - borderColor: colorAction.default.foreground, + backgroundColor: states.default.background, + borderColor: states.default.border, borderWidth: size / 4, // Focus and hover have the same style. Focus style only shows // up with keyboard navigation. ":focus-visible": { + // TODO(WB-1856): Use the correct border color for focus outline: `${border.width.thin}px solid ${colorAction.hover.border}`, outlineOffset: 1, }, @@ -129,28 +153,27 @@ const _generateStyles = (checked: Checked, error: boolean) => { }, }; } else { + const currentState = error ? states.error : states.default; + newStyles = { default: { - backgroundColor: error - ? semanticColor.status.critical.background - : colorAction.default.background, - borderColor: error - ? semanticColor.status.critical.foreground - : colorAction.default.border, + backgroundColor: currentState.background, + borderColor: currentState.border, // Focus and hover have the same style. Focus style only shows // up with keyboard navigation. ":focus-visible": { backgroundColor: error - ? semanticColor.status.critical.background + ? states.error.background : colorAction.hover.background, + // TODO(WB-1856): Use the correct border color for focus outline: `${border.width.thin}px solid ${colorAction.hover.border}`, outlineOffset: -1, }, ":hover": { backgroundColor: error - ? semanticColor.status.critical.background + ? states.error.background : colorAction.hover.background, outline: `${border.width.thin}px solid ${colorAction.hover.border}`, outlineOffset: -1, From 2c1355473f333fe059438b48478a018909c8388a Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Tue, 28 Jan 2025 15:47:39 -0500 Subject: [PATCH 09/11] [form-cb-rb-semantic-v2] Fix styles (Radio) --- .../src/components/checkbox-core.tsx | 2 +- .../src/components/radio-core.tsx | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/wonder-blocks-form/src/components/checkbox-core.tsx b/packages/wonder-blocks-form/src/components/checkbox-core.tsx index 8029ae284..0e3404f10 100644 --- a/packages/wonder-blocks-form/src/components/checkbox-core.tsx +++ b/packages/wonder-blocks-form/src/components/checkbox-core.tsx @@ -180,7 +180,7 @@ const _generateStyles = (checked: Checked, error: boolean) => { // The different states that the component can be in. const states = { - // Resting state + // Resting state (shared between checked and unchecked) default: { border: colorAction.default.border, background: colorAction.default.background, diff --git a/packages/wonder-blocks-form/src/components/radio-core.tsx b/packages/wonder-blocks-form/src/components/radio-core.tsx index 0df5909f2..95af12623 100644 --- a/packages/wonder-blocks-form/src/components/radio-core.tsx +++ b/packages/wonder-blocks-form/src/components/radio-core.tsx @@ -109,8 +109,12 @@ const _generateStyles = (checked: Checked, error: boolean) => { // The different states that the component can be in. const states = { - // Resting state - default: { + // Resting state (unchecked) + unchecked: { + border: semanticColor.border.strong, + background: colorAction.default.background, + }, + checked: { // NOTE: This is a special case where the border is the same color // as the foreground. This should change as soon as we simplify the // existing `action` tokens. @@ -128,8 +132,8 @@ const _generateStyles = (checked: Checked, error: boolean) => { if (checked) { newStyles = { default: { - backgroundColor: states.default.background, - borderColor: states.default.border, + backgroundColor: states.checked.background, + borderColor: states.checked.border, borderWidth: size / 4, // Focus and hover have the same style. Focus style only shows @@ -153,7 +157,7 @@ const _generateStyles = (checked: Checked, error: boolean) => { }, }; } else { - const currentState = error ? states.error : states.default; + const currentState = error ? states.error : states.unchecked; newStyles = { default: { From fad122b709fa97bbe73d5dfde494120566c5f459 Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Tue, 28 Jan 2025 17:12:02 -0500 Subject: [PATCH 10/11] [form-cb-rb-semantic-v2] Use semantic focus border color --- .../wonder-blocks-form/src/components/checkbox-core.tsx | 8 ++++---- packages/wonder-blocks-form/src/components/radio-core.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/wonder-blocks-form/src/components/checkbox-core.tsx b/packages/wonder-blocks-form/src/components/checkbox-core.tsx index 0e3404f10..215b593d6 100644 --- a/packages/wonder-blocks-form/src/components/checkbox-core.tsx +++ b/packages/wonder-blocks-form/src/components/checkbox-core.tsx @@ -203,8 +203,8 @@ const _generateStyles = (checked: Checked, error: boolean) => { // Focus and hover have the same style. Focus style only shows // up with keyboard navigation. ":focus-visible": { - // TODO(WB-1856): Use the correct border color for focus - outline: `${border.width.thin}px solid ${colorAction.hover.border}`, + // TODO(WB-1856): Define global pattern for focus styles + outline: `${border.width.thin}px solid ${semanticColor.border.focus}`, outlineOffset: 1, }, @@ -235,8 +235,8 @@ const _generateStyles = (checked: Checked, error: boolean) => { backgroundColor: error ? states.error.background : colorAction.hover.background, - // TODO(WB-1856): Use the correct border color for focus - outline: `${border.width.thin}px solid ${colorAction.hover.border}`, + // TODO(WB-1856): Define global pattern for focus styles + outline: `${border.width.thin}px solid ${semanticColor.border.focus}`, outlineOffset: -1, }, diff --git a/packages/wonder-blocks-form/src/components/radio-core.tsx b/packages/wonder-blocks-form/src/components/radio-core.tsx index 95af12623..cd14ea108 100644 --- a/packages/wonder-blocks-form/src/components/radio-core.tsx +++ b/packages/wonder-blocks-form/src/components/radio-core.tsx @@ -139,8 +139,8 @@ const _generateStyles = (checked: Checked, error: boolean) => { // Focus and hover have the same style. Focus style only shows // up with keyboard navigation. ":focus-visible": { - // TODO(WB-1856): Use the correct border color for focus - outline: `${border.width.thin}px solid ${colorAction.hover.border}`, + // TODO(WB-1856): Define global pattern for focus styles + outline: `${border.width.thin}px solid ${semanticColor.border.focus}`, outlineOffset: 1, }, @@ -170,8 +170,8 @@ const _generateStyles = (checked: Checked, error: boolean) => { backgroundColor: error ? states.error.background : colorAction.hover.background, - // TODO(WB-1856): Use the correct border color for focus - outline: `${border.width.thin}px solid ${colorAction.hover.border}`, + // TODO(WB-1856): Define global pattern for focus styles + outline: `${border.width.thin}px solid ${semanticColor.border.focus}`, outlineOffset: -1, }, From 0d9c7f15d5377bc76bd59a8767d5bf82c036de5c Mon Sep 17 00:00:00 2001 From: Juan Andrade Date: Tue, 28 Jan 2025 17:18:39 -0500 Subject: [PATCH 11/11] [form-cb-rb-semantic-v2] Revert semantic focus color --- packages/wonder-blocks-form/src/components/checkbox-core.tsx | 4 ++-- packages/wonder-blocks-form/src/components/radio-core.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/wonder-blocks-form/src/components/checkbox-core.tsx b/packages/wonder-blocks-form/src/components/checkbox-core.tsx index 215b593d6..631c37ea9 100644 --- a/packages/wonder-blocks-form/src/components/checkbox-core.tsx +++ b/packages/wonder-blocks-form/src/components/checkbox-core.tsx @@ -204,7 +204,7 @@ const _generateStyles = (checked: Checked, error: boolean) => { // up with keyboard navigation. ":focus-visible": { // TODO(WB-1856): Define global pattern for focus styles - outline: `${border.width.thin}px solid ${semanticColor.border.focus}`, + outline: `${border.width.thin}px solid ${colorAction.hover.border}`, outlineOffset: 1, }, @@ -236,7 +236,7 @@ const _generateStyles = (checked: Checked, error: boolean) => { ? states.error.background : colorAction.hover.background, // TODO(WB-1856): Define global pattern for focus styles - outline: `${border.width.thin}px solid ${semanticColor.border.focus}`, + outline: `${border.width.thin}px solid ${colorAction.hover.border}`, outlineOffset: -1, }, diff --git a/packages/wonder-blocks-form/src/components/radio-core.tsx b/packages/wonder-blocks-form/src/components/radio-core.tsx index cd14ea108..a0c2b4cc9 100644 --- a/packages/wonder-blocks-form/src/components/radio-core.tsx +++ b/packages/wonder-blocks-form/src/components/radio-core.tsx @@ -140,7 +140,7 @@ const _generateStyles = (checked: Checked, error: boolean) => { // up with keyboard navigation. ":focus-visible": { // TODO(WB-1856): Define global pattern for focus styles - outline: `${border.width.thin}px solid ${semanticColor.border.focus}`, + outline: `${border.width.thin}px solid ${colorAction.hover.border}`, outlineOffset: 1, }, @@ -171,7 +171,7 @@ const _generateStyles = (checked: Checked, error: boolean) => { ? states.error.background : colorAction.hover.background, // TODO(WB-1856): Define global pattern for focus styles - outline: `${border.width.thin}px solid ${semanticColor.border.focus}`, + outline: `${border.width.thin}px solid ${colorAction.hover.border}`, outlineOffset: -1, },