diff --git a/.changeset/nervous-llamas-ring.md b/.changeset/nervous-llamas-ring.md new file mode 100644 index 00000000000..07228fa5c67 --- /dev/null +++ b/.changeset/nervous-llamas-ring.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Update Heading component to use CSS Modules behind feature flag diff --git a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-colorblind-linux.png b/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-colorblind-linux.png deleted file mode 100644 index 97073ba1756..00000000000 Binary files a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-dimmed-linux.png b/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-dimmed-linux.png deleted file mode 100644 index d4989685c17..00000000000 Binary files a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-dimmed-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-high-contrast-linux.png b/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-high-contrast-linux.png deleted file mode 100644 index b42efcc867b..00000000000 Binary files a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-linux.png b/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-linux.png deleted file mode 100644 index 97073ba1756..00000000000 Binary files a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-tritanopia-linux.png b/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-tritanopia-linux.png deleted file mode 100644 index 97073ba1756..00000000000 Binary files a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-dark-tritanopia-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-light-colorblind-linux.png b/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-light-colorblind-linux.png deleted file mode 100644 index 37f69f263e6..00000000000 Binary files a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-light-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-light-high-contrast-linux.png b/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-light-high-contrast-linux.png deleted file mode 100644 index 8f99e09a911..00000000000 Binary files a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-light-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-light-linux.png b/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-light-linux.png deleted file mode 100644 index 37f69f263e6..00000000000 Binary files a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-light-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-light-tritanopia-linux.png b/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-light-tritanopia-linux.png deleted file mode 100644 index 37f69f263e6..00000000000 Binary files a/.playwright/snapshots/components/Heading.test.ts-snapshots/Heading-Default-light-tritanopia-linux.png and /dev/null differ diff --git a/e2e/components/Heading.test.ts b/e2e/components/Heading.test.ts index 9d13fb2110a..598092f6236 100644 --- a/e2e/components/Heading.test.ts +++ b/e2e/components/Heading.test.ts @@ -1,74 +1,69 @@ import {test, expect} from '@playwright/test' import {visit} from '../test-helpers/storybook' -test.describe('Heading', () => { - test.describe('Default', () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-heading--default', - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`Heading.Default.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-heading--default', - }) - await expect(page).toHaveNoViolations() - }) - }) - - test.describe('Small', () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-heading-features--small', - }) +const stories = [ + { + title: 'Default', + id: 'components-heading--default', + }, + { + title: 'Small', + id: 'components-heading-features--small', + }, + { + title: 'Medium', + id: 'components-heading-features--medium', + }, + { + title: 'Large', + id: 'components-heading-features--large', + }, +] as const - expect(await page.screenshot()).toMatchSnapshot(`Heading.Small.png`) - }) +test.describe('Heading', () => { + for (const story of stories) { + test.describe(story.title, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: story.id, + globals: { + featureFlags: { + primer_react_css_modules: true, + }, + }, + }) - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-heading-features--small', + // Default state + expect(await page.screenshot()).toMatchSnapshot(`Heading.${story.title}.png`) }) - await expect(page).toHaveNoViolations() - }) - }) - test.describe('Medium', () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-heading-features--medium', - }) + test('default (styled-components) @vrt', async ({page}) => { + await visit(page, { + id: story.id, + }) - expect(await page.screenshot()).toMatchSnapshot(`Heading.Medium.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-heading-features--medium', + // Default state + expect(await page.screenshot()).toMatchSnapshot(`Heading.${story.title}.png`) }) - await expect(page).toHaveNoViolations() - }) - }) - test.describe('Large', () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-heading-features--large', + test('axe @aat', async ({page}) => { + await visit(page, { + id: story.id, + globals: { + featureFlags: { + primer_react_css_modules: true, + }, + }, + }) + await expect(page).toHaveNoViolations() }) - // Default state - expect(await page.screenshot()).toMatchSnapshot(`Heading.Large.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-heading-features--large', + test('axe (styled-components) @aat', async ({page}) => { + await visit(page, { + id: story.id, + }) + await expect(page).toHaveNoViolations() }) - await expect(page).toHaveNoViolations() }) - }) + } }) diff --git a/packages/react/src/Heading/Heading.module.css b/packages/react/src/Heading/Heading.module.css new file mode 100644 index 00000000000..d93ee2bc93c --- /dev/null +++ b/packages/react/src/Heading/Heading.module.css @@ -0,0 +1,17 @@ +.Heading { + margin: 0; + font-size: var(--text-title-size-large); + font-weight: var(--base-text-weight-semibold); + + &[data-variant='large'] { + font: var(--text-title-shorthand-large); + } + + &[data-variant='medium'] { + font: var(--text-title-shorthand-medium); + } + + &[data-variant='small'] { + font: var(--text-title-shorthand-small); + } +} diff --git a/packages/react/src/Heading/Heading.tsx b/packages/react/src/Heading/Heading.tsx index 78c14c77f58..d58539ad75e 100644 --- a/packages/react/src/Heading/Heading.tsx +++ b/packages/react/src/Heading/Heading.tsx @@ -1,3 +1,4 @@ +import cx from 'clsx' import React, {forwardRef, useEffect} from 'react' import styled from 'styled-components' import {get} from '../constants' @@ -6,6 +7,9 @@ import type {SxProp} from '../sx' import sx from '../sx' import type {ComponentProps} from '../utils/types' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' +import classes from './Heading.module.css' +import {useFeatureFlag} from '../FeatureFlags' +import Box from '../Box' type StyledHeadingProps = { as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' @@ -28,9 +32,12 @@ const StyledHeading = styled.h2` &:where([data-variant='small']) { font: var(--text-title-shorthand-small, 600 16px / 1.5 ${get('fonts.normal')}); } + ${sx}; ` -const Heading = forwardRef(({as: Component = 'h2', variant, ...props}, forwardedRef) => { + +const Heading = forwardRef(({as: Component = 'h2', className, variant, ...props}, forwardedRef) => { + const enabled = useFeatureFlag('primer_react_css_modules') const innerRef = React.useRef(null) useRefObjectAsForwardedRef(forwardedRef, innerRef) @@ -50,13 +57,31 @@ const Heading = forwardRef(({as: Component = 'h2', variant, ...props}, forwarded }, [innerRef]) } + if (enabled) { + if (props.sx) { + return ( + + ) + } + return + } + return ( ) }) as PolymorphicForwardRefComponent<'h2', StyledHeadingProps> diff --git a/packages/react/src/Heading/__tests__/Heading.test.tsx b/packages/react/src/Heading/__tests__/Heading.test.tsx index 1be9598a654..ac4ae84f7fd 100644 --- a/packages/react/src/Heading/__tests__/Heading.test.tsx +++ b/packages/react/src/Heading/__tests__/Heading.test.tsx @@ -1,9 +1,10 @@ import React from 'react' import {Heading} from '../..' import {render, behavesAsComponent, checkExports} from '../../utils/testing' -import {render as HTMLRender} from '@testing-library/react' +import {render as HTMLRender, screen} from '@testing-library/react' import axe from 'axe-core' import ThemeProvider from '../../ThemeProvider' +import {FeatureFlags} from '../../FeatureFlags' const theme = { breakpoints: ['400px', '640px', '960px', '1280px'], @@ -140,4 +141,55 @@ describe('Heading', () => { ), ).toHaveStyleRule('font-style', 'italic') }) + + describe('with primer_react_css_modules enabled', () => { + it('should only include css modules class', () => { + HTMLRender( + + test + , + ) + expect(screen.getByText('test')).toHaveClass('Heading') + // Note: this is the generated class name when styled-components is used + // for this component + expect(screen.getByText('test')).not.toHaveClass(/^Heading__StyledHeading/) + }) + + it('should support `className` on the outermost element', () => { + const {container} = HTMLRender( + + test + , + ) + expect(container.firstChild).toHaveClass('test') + }) + + it('should support overrides with sx if provided', () => { + HTMLRender( + + + test + + , + ) + + expect(screen.getByText('test')).toHaveStyle('font-weight: 900') + }) + }) })