diff --git a/.changeset/new-pans-shout.md b/.changeset/new-pans-shout.md new file mode 100644 index 00000000000..961312476d0 --- /dev/null +++ b/.changeset/new-pans-shout.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +Convert ActionList.Heading to CSS Modules diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-colorblind-linux.png new file mode 100644 index 00000000000..c35d8aa199d Binary files /dev/null and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-dimmed-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-dimmed-linux.png new file mode 100644 index 00000000000..5964c04027b Binary files /dev/null and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-high-contrast-linux.png new file mode 100644 index 00000000000..213f7b60742 Binary files /dev/null and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-linux.png new file mode 100644 index 00000000000..c35d8aa199d Binary files /dev/null and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-tritanopia-linux.png new file mode 100644 index 00000000000..c35d8aa199d Binary files /dev/null and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-colorblind-linux.png new file mode 100644 index 00000000000..53b577bddc1 Binary files /dev/null and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-high-contrast-linux.png new file mode 100644 index 00000000000..4ca17804028 Binary files /dev/null and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-linux.png new file mode 100644 index 00000000000..53b577bddc1 Binary files /dev/null and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-tritanopia-linux.png new file mode 100644 index 00000000000..53b577bddc1 Binary files /dev/null and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-tritanopia-linux.png differ diff --git a/e2e/components/ActionList.test.ts b/e2e/components/ActionList.test.ts index 6d10201126d..3da80621f8a 100644 --- a/e2e/components/ActionList.test.ts +++ b/e2e/components/ActionList.test.ts @@ -712,4 +712,32 @@ test.describe('ActionList', () => { }) } }) + + test.describe('Heading with Classname', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-actionlist-dev--heading-custom-classname', + globals: { + colorScheme: theme, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`Heading with Classname.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-actionlist-dev--heading-custom-classname', + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations() + }) + }) + } + }) }) diff --git a/packages/react/src/ActionList/ActionList.dev.stories.tsx b/packages/react/src/ActionList/ActionList.dev.stories.tsx index 6d0fc3d1da7..ab7de7a1157 100644 --- a/packages/react/src/ActionList/ActionList.dev.stories.tsx +++ b/packages/react/src/ActionList/ActionList.dev.stories.tsx @@ -116,3 +116,16 @@ export const GroupHeadingCustomClassname = () => ( ) + +export const HeadingCustomClassname = () => ( + + + Filter by + + + Repositories + {}}>app/assets/modules + {}}>src/react/components + + +) diff --git a/packages/react/src/ActionList/ActionList.features.stories.tsx b/packages/react/src/ActionList/ActionList.features.stories.tsx index 18c5393042c..2cb63ac8fff 100644 --- a/packages/react/src/ActionList/ActionList.features.stories.tsx +++ b/packages/react/src/ActionList/ActionList.features.stories.tsx @@ -50,7 +50,9 @@ export const SimpleList = () => ( export const WithVisualListHeading = () => ( - Filter by + + Filter by + Repositories {}}> diff --git a/packages/react/src/ActionList/ActionList.test.tsx b/packages/react/src/ActionList/ActionList.test.tsx index ae983870427..bf1dfdffae1 100644 --- a/packages/react/src/ActionList/ActionList.test.tsx +++ b/packages/react/src/ActionList/ActionList.test.tsx @@ -237,52 +237,6 @@ describe('ActionList', () => { expect(onClick).toHaveBeenCalled() }) - it('should render the ActionList.Heading component as a heading with the given heading level', async () => { - const container = HTMLRender( - - Heading - , - ) - const heading = container.getByRole('heading', {level: 1}) - expect(heading).toBeInTheDocument() - expect(heading).toHaveTextContent('Heading') - }) - it('should label the action list with the heading id', async () => { - const {container, getByRole} = HTMLRender( - - Heading - Item - , - ) - const list = container.querySelector('ul') - const heading = getByRole('heading', {level: 1}) - expect(list).toHaveAttribute('aria-labelledby', heading.id) - }) - it('should throw an error when ActionList.Heading is used within ActionMenu context', async () => { - const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) - expect(() => - HTMLRender( - - - - Trigger - - - Heading - Item - - - - - , - ), - ).toThrow( - "ActionList.Heading shouldn't be used within an ActionMenu container. Menus are labelled by the menu button's name.", - ) - expect(spy).toHaveBeenCalled() - spy.mockRestore() - }) - it('should throw an error when ActionList.GroupHeading has an `as` prop when it is used within ActionMenu context', async () => { const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) expect(() => diff --git a/packages/react/src/ActionList/Heading.module.css b/packages/react/src/ActionList/Heading.module.css new file mode 100644 index 00000000000..fffceddac23 --- /dev/null +++ b/packages/react/src/ActionList/Heading.module.css @@ -0,0 +1,12 @@ +.ActionListHeader { + margin-block-end: var(--base-size-8); + + &:where([data-list-variant='full']) { + margin-inline-start: var(--base-size-8); + } + + &:where([data-list-variant='inset']) { + /* stylelint-disable-next-line primer/spacing */ + margin-inline-start: calc(var(--control-medium-paddingInline-condensed) + var(--base-size-8)); + } +} diff --git a/packages/react/src/ActionList/Heading.test.tsx b/packages/react/src/ActionList/Heading.test.tsx new file mode 100644 index 00000000000..d8a2e6cbb98 --- /dev/null +++ b/packages/react/src/ActionList/Heading.test.tsx @@ -0,0 +1,83 @@ +import {render as HTMLRender} from '@testing-library/react' +import React from 'react' +import theme from '../theme' +import {ActionList} from '.' +import {BaseStyles, ThemeProvider, ActionMenu} from '..' +import {FeatureFlags} from '../FeatureFlags' + +describe('ActionList.Heading', () => { + it('should render the ActionList.Heading component as a heading with the given heading level', async () => { + const container = HTMLRender( + + Heading + , + ) + const heading = container.getByRole('heading', {level: 1}) + expect(heading).toBeInTheDocument() + expect(heading).toHaveTextContent('Heading') + }) + + it('should label the action list with the heading id', async () => { + const {container, getByRole} = HTMLRender( + + Heading + Item + , + ) + const list = container.querySelector('ul') + const heading = getByRole('heading', {level: 1}) + expect(list).toHaveAttribute('aria-labelledby', heading.id) + }) + + it('should throw an error when ActionList.Heading is used within ActionMenu context', async () => { + const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) + expect(() => + HTMLRender( + + + + Trigger + + + Heading + Item + + + + + , + ), + ).toThrow( + "ActionList.Heading shouldn't be used within an ActionMenu container. Menus are labelled by the menu button's name.", + ) + expect(spy).toHaveBeenCalled() + spy.mockRestore() + }) + + it('should support a custom `className` on the outermost element', () => { + const Element = () => { + return ( + + + Filter by + + + ) + } + const FeatureFlagElement = () => { + return ( + + + + ) + } + expect(HTMLRender().container.querySelector('h2')).toHaveClass('test-class-name') + expect(HTMLRender().container.querySelector('h2')).toHaveClass('test-class-name') + }) +}) diff --git a/packages/react/src/ActionList/Heading.tsx b/packages/react/src/ActionList/Heading.tsx index 1971f9a3886..d024be646c7 100644 --- a/packages/react/src/ActionList/Heading.tsx +++ b/packages/react/src/ActionList/Heading.tsx @@ -9,18 +9,26 @@ import {ListContext} from './shared' import VisuallyHidden from '../_VisuallyHidden' import {ActionListContainerContext} from './ActionListContainerContext' import {invariant} from '../utils/invariant' +import {clsx} from 'clsx' +import {useFeatureFlag} from '../FeatureFlags' +import classes from './Heading.module.css' type HeadingLevels = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' +type HeadingVariants = 'large' | 'medium' | 'small' export type ActionListHeadingProps = { as: HeadingLevels + size?: HeadingVariants visuallyHidden?: boolean + className?: string } & SxProp export const Heading = forwardRef( - ({as, children, sx = defaultSxProp, visuallyHidden = false, ...props}, forwardedRef) => { + ({as, size, children, sx = defaultSxProp, visuallyHidden = false, className, ...props}, forwardedRef) => { const innerRef = React.useRef(null) useRefObjectAsForwardedRef(forwardedRef, innerRef) + const enabled = useFeatureFlag('primer_react_css_modules_team') + const {headingId: headingId, variant: listVariant} = React.useContext(ListContext) const {container} = React.useContext(ActionListContainerContext) @@ -37,16 +45,49 @@ export const Heading = forwardRef( return ( - (styles, sx)} - {...props} - > - {children} - + {enabled ? ( + sx !== defaultSxProp ? ( + + {children} + + ) : ( + + {children} + + ) + ) : ( + (styles, sx)} + className={className} + {...props} + > + {children} + + )} ) },