diff --git a/.changeset/lovely-stingrays-jog.md b/.changeset/lovely-stingrays-jog.md new file mode 100644 index 00000000000..a4e1a140c03 --- /dev/null +++ b/.changeset/lovely-stingrays-jog.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +Convert CheckBoxOrRadioGroup to CSS modules behind feature flag diff --git a/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-colorblind-linux.png b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-colorblind-linux.png new file mode 100644 index 00000000000..0781fa11301 Binary files /dev/null and b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-dimmed-linux.png b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-dimmed-linux.png new file mode 100644 index 00000000000..c03b056cd71 Binary files /dev/null and b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-high-contrast-linux.png b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-high-contrast-linux.png new file mode 100644 index 00000000000..b21528c4536 Binary files /dev/null and b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-linux.png b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-linux.png new file mode 100644 index 00000000000..0781fa11301 Binary files /dev/null and b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-linux.png differ diff --git a/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-tritanopia-linux.png b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-tritanopia-linux.png new file mode 100644 index 00000000000..0781fa11301 Binary files /dev/null and b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-light-colorblind-linux.png b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-light-colorblind-linux.png new file mode 100644 index 00000000000..0c15cb0782d Binary files /dev/null and b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-light-high-contrast-linux.png b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-light-high-contrast-linux.png new file mode 100644 index 00000000000..b3f2160a5d4 Binary files /dev/null and b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-light-linux.png b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-light-linux.png new file mode 100644 index 00000000000..633f5b8f7c3 Binary files /dev/null and b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-light-linux.png differ diff --git a/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-light-tritanopia-linux.png b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-light-tritanopia-linux.png new file mode 100644 index 00000000000..0c15cb0782d Binary files /dev/null and b/.playwright/snapshots/components/CheckboxGroup.test.ts-snapshots/CheckboxGroup-SX-Props-light-tritanopia-linux.png differ diff --git a/e2e/components/CheckboxGroup.test.ts b/e2e/components/CheckboxGroup.test.ts index 45697bbeba5..8b5faa1d261 100644 --- a/e2e/components/CheckboxGroup.test.ts +++ b/e2e/components/CheckboxGroup.test.ts @@ -2,176 +2,69 @@ import {test, expect} from '@playwright/test' import {visit} from '../test-helpers/storybook' import {themes} from '../test-helpers/themes' -test.describe('CheckboxGroup', () => { - test.describe('Default', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-checkboxgroup--default', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(`CheckboxGroup.Default.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-checkboxgroup--default', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: false, - }, - }, - }) - }) - }) - } - }) +const stories = [ + { + title: 'Default', + id: 'components-checkboxgroup--default', + }, + { + title: 'Caption', + id: 'components-checkboxgroup-features--caption', + }, + { + title: 'Error', + id: 'components-checkboxgroup-features--error', + }, + { + title: 'Success', + id: 'components-checkboxgroup-features--success', + }, + { + title: 'Visually Hidden Label', + id: 'components-checkboxgroup-features--visually-hidden-label', + }, + { + title: 'SX Props', + id: 'components-checkboxgroup-dev--sx-props', + }, +] as const - test.describe('Caption', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-checkboxgroup-features--caption', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(`CheckboxGroup.Caption.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-checkboxgroup-features--caption', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: false, - }, - }, - }) - }) - }) - } - }) - - test.describe('Error', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-checkboxgroup-features--error', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(`CheckboxGroup.Error.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-checkboxgroup-features--error', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: false, +test.describe('CheckboxGroup', () => { + for (const story of stories) { + test.describe(story.title, () => { + for (const theme of themes) { + test.describe(theme, () => { + test('@vrt', async ({page}) => { + await visit(page, { + id: story.id, + globals: { + colorScheme: theme, }, - }, - }) - }) - }) - } - }) + }) - test.describe('Success', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-checkboxgroup-features--success', - globals: { - colorScheme: theme, - }, + // Default state + expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot( + `CheckboxGroup.${story.title}.${theme}.png`, + ) }) - // Default state - expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(`CheckboxGroup.Success.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-checkboxgroup-features--success', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: false, + test('axe @aat', async ({page}) => { + await visit(page, { + id: story.id, + globals: { + colorScheme: theme, }, - }, - }) - }) - }) - } - }) - - test.describe('Visually Hidden Label', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-checkboxgroup-features--visually-hidden-label', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot( - `CheckboxGroup.Visually Hidden Label.${theme}.png`, - ) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-checkboxgroup-features--visually-hidden-label', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: false, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: false, + }, }, - }, + }) }) }) - }) - } - }) + } + }) + } }) diff --git a/packages/react/src/CheckboxGroup/CheckboxGroup.dev.stories.tsx b/packages/react/src/CheckboxGroup/CheckboxGroup.dev.stories.tsx new file mode 100644 index 00000000000..333c3ef0cea --- /dev/null +++ b/packages/react/src/CheckboxGroup/CheckboxGroup.dev.stories.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import type {Meta} from '@storybook/react' +import {Checkbox, CheckboxGroup, FormControl} from '..' + +export default { + title: 'Components/CheckboxGroup/Dev', + component: CheckboxGroup, + parameters: {controls: {exclude: ['aria-labelledby', 'id', 'onChange', 'sx']}}, +} as Meta + +export const SxProps = () => ( + + + Caption + + + Choices + + + + Choice one + + + + Choice two + + + + Choice three + + +) diff --git a/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroup.module.css b/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroup.module.css new file mode 100644 index 00000000000..e48b26bda5c --- /dev/null +++ b/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroup.module.css @@ -0,0 +1,47 @@ +.Body { + display: flex; + padding: 0; + margin: 0; + list-style: none; + flex-direction: column; + + & > * + * { + margin-top: var(--base-size-8); + } +} + +.GroupFieldset { + padding: 0; + margin: 0; + border: none; + + &:where([data-validation]) { + margin-bottom: var(--base-size-8); + } +} + +.GroupLegend { + padding: 0; + + &:where([data-legend-visible]) { + margin-bottom: var(--base-size-8); + } +} + +.CheckboxOrRadioGroupCaption { + font-size: var(--text-body-size-medium); + color: var(--fgColor-muted); +} + +.RadioGroupLabel { + display: block; + font-size: var(--text-body-size-large); + + &:where([data-label-disabled]) { + color: var(--fgColor-muted); + } +} + +.GroupLabelChildren { + margin-right: var(--base-size-8); +} diff --git a/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx b/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx index d4958833da6..dbe52aca196 100644 --- a/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx +++ b/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx @@ -11,8 +11,15 @@ import CheckboxOrRadioGroupContext from './CheckboxOrRadioGroupContext' import VisuallyHidden from '../../../_VisuallyHidden' import {useSlots} from '../../../hooks/useSlots' import type {SxProp} from '../../../sx' +import classes from './CheckboxOrRadioGroup.module.css' +import {toggleStyledComponent} from '../../utils/toggleStyledComponent' +import {useFeatureFlag} from '../../../FeatureFlags' +import {clsx} from 'clsx' +import {CSS_MODULES_FLAG} from './FeatureFlag' export type CheckboxOrRadioGroupProps = { + /** Class name for custom styling */ + className?: string /** * Used when associating the input group with a label other than `CheckboxOrRadioGroup.Label` */ @@ -32,17 +39,21 @@ export type CheckboxOrRadioGroupProps = { required?: boolean } & SxProp -const Body = styled.div` - display: flex; - flex-direction: column; - list-style: none; - margin: 0; - padding: 0; +const Body = toggleStyledComponent( + CSS_MODULES_FLAG, + 'div', + styled.div` + display: flex; + flex-direction: column; + list-style: none; + margin: 0; + padding: 0; - > * + * { - margin-top: ${get('space.2')}; - } -` + > * + * { + margin-top: ${get('space.2')}; + } + `, +) const CheckboxOrRadioGroup: React.FC> = ({ 'aria-labelledby': ariaLabelledby, @@ -50,6 +61,7 @@ const CheckboxOrRadioGroup: React.FC { const [slots, rest] = useSlots(children, { @@ -79,6 +91,142 @@ const CheckboxOrRadioGroup: React.FC +
+ + {labelChild ? ( + /* + Placing the caption text and validation text in the provides a better user + experience for more screenreaders. + + Reference: https://blog.tenon.io/accessible-validation-of-checkbox-and-radiobutton-groups/ + */ + + {slots.label} + {slots.caption} + {React.isValidElement(slots.validation) && slots.validation.props.children && ( + {slots.validation.props.children} + )} + + ) : ( + /* + If CheckboxOrRadioGroup.Label wasn't passed as a child, we don't render a + but we still want to render a caption + */ + slots.caption + )} + + + {React.Children.toArray(rest).filter(child => React.isValidElement(child))} + + + {validationChild && ( + + aria-hidden={Boolean(labelChild)} + show + > + {slots.validation} + + )} +
+ + ) + } + return ( + +
+
+ {labelChild ? ( + /* + Placing the caption text and validation text in the provides a better user + experience for more screenreaders. + + Reference: https://blog.tenon.io/accessible-validation-of-checkbox-and-radiobutton-groups/ + */ + + {slots.label} + {slots.caption} + {React.isValidElement(slots.validation) && slots.validation.props.children && ( + {slots.validation.props.children} + )} + + ) : ( + /* + If CheckboxOrRadioGroup.Label wasn't passed as a child, we don't render a + but we still want to render a caption + */ + slots.caption + )} + + + {React.Children.toArray(rest).filter(child => React.isValidElement(child))} + +
+ {validationChild && ( + + aria-hidden={Boolean(labelChild)} + show + > + {slots.validation} + + )} +
+
+ ) + } + return ( {labelChild ? ( diff --git a/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupCaption.tsx b/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupCaption.tsx index 683e32c002d..4e74d4fc6bb 100644 --- a/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupCaption.tsx +++ b/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupCaption.tsx @@ -2,11 +2,33 @@ import React from 'react' import Text from '../../../Text' import type {SxProp} from '../../../sx' import CheckboxOrRadioGroupContext from './CheckboxOrRadioGroupContext' +import classes from './CheckboxOrRadioGroup.module.css' +import {CSS_MODULES_FLAG} from './FeatureFlag' +import {useFeatureFlag} from '../../../FeatureFlags' +import {clsx} from 'clsx' -const CheckboxOrRadioGroupCaption: React.FC> = ({children, sx}) => { +type CheckboxOrRadioGroupCaptionProps = React.PropsWithChildren & {className?: string} + +const CheckboxOrRadioGroupCaption: React.FC = ({className, children, sx}) => { const {disabled, captionId} = React.useContext(CheckboxOrRadioGroupContext) + const enabled = useFeatureFlag(CSS_MODULES_FLAG) + if (enabled) { + if (sx) { + return ( + + {children} + + ) + } + return ( + + {children} + + ) + } + return ( - + {children} ) diff --git a/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupLabel.tsx b/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupLabel.tsx index 2f5b43ea0f7..ee79f687cc1 100644 --- a/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupLabel.tsx +++ b/packages/react/src/internal/components/CheckboxOrRadioGroup/CheckboxOrRadioGroupLabel.tsx @@ -3,8 +3,15 @@ import Box from '../../../Box' import VisuallyHidden from '../../../_VisuallyHidden' import type {SxProp} from '../../../sx' import CheckboxOrRadioGroupContext from './CheckboxOrRadioGroupContext' +import {CSS_MODULES_FLAG} from './FeatureFlag' +import {useFeatureFlag} from '../../../FeatureFlags' +import classes from './CheckboxOrRadioGroup.module.css' +import {Stack} from '../../../Stack' +import {clsx} from 'clsx' export type CheckboxOrRadioGroupLabelProps = { + /** Class name for custom styling */ + className?: string /** * Whether to visually hide the fieldset legend */ @@ -13,12 +20,57 @@ export type CheckboxOrRadioGroupLabelProps = { const CheckboxOrRadioGroupLabel: React.FC> = ({ children, + className, visuallyHidden = false, sx, }) => { const {required, disabled} = React.useContext(CheckboxOrRadioGroupContext) + const enabled = useFeatureFlag(CSS_MODULES_FLAG) + + if (enabled) { + if (sx) { + return ( + + {required ? ( + +
{children}
+ * +
+ ) : ( + children + )} +
+ ) + } + + return ( + + {required ? ( + +
{children}
+ * +
+ ) : ( + children + )} +
+ ) + } + return (