diff --git a/.changeset/small-foxes-tan.md b/.changeset/small-foxes-tan.md new file mode 100644 index 00000000000..bc5ee27c455 --- /dev/null +++ b/.changeset/small-foxes-tan.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +Convert ProgressBar to CSS modules behind feature flag diff --git a/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-colorblind-linux.png b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-colorblind-linux.png new file mode 100644 index 00000000000..6fe55fca51d Binary files /dev/null and b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-dimmed-linux.png b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-dimmed-linux.png new file mode 100644 index 00000000000..1aa04d731a1 Binary files /dev/null and b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-high-contrast-linux.png b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-high-contrast-linux.png new file mode 100644 index 00000000000..ea3000d0cfd Binary files /dev/null and b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-linux.png b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-linux.png new file mode 100644 index 00000000000..b272c909a6f Binary files /dev/null and b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-linux.png differ diff --git a/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-tritanopia-linux.png b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-tritanopia-linux.png new file mode 100644 index 00000000000..6fe55fca51d Binary files /dev/null and b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-light-colorblind-linux.png b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-light-colorblind-linux.png new file mode 100644 index 00000000000..1fcdfa55c72 Binary files /dev/null and b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-light-high-contrast-linux.png b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-light-high-contrast-linux.png new file mode 100644 index 00000000000..d828f979366 Binary files /dev/null and b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-light-linux.png b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-light-linux.png new file mode 100644 index 00000000000..baf2d5630f7 Binary files /dev/null and b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-light-linux.png differ diff --git a/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-light-tritanopia-linux.png b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-light-tritanopia-linux.png new file mode 100644 index 00000000000..1fcdfa55c72 Binary files /dev/null and b/.playwright/snapshots/components/ProgressBar.test.ts-snapshots/ProgressBar-Dev-SX-Props-light-tritanopia-linux.png differ diff --git a/e2e/components/ProgressBar.test.ts b/e2e/components/ProgressBar.test.ts index ac87493b2a1..acef0768594 100644 --- a/e2e/components/ProgressBar.test.ts +++ b/e2e/components/ProgressBar.test.ts @@ -2,228 +2,75 @@ import {test, expect} from '@playwright/test' import {visit} from '../test-helpers/storybook' import {themes} from '../test-helpers/themes' -test.describe('ProgressBar', () => { - test.describe('Default', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-progressbar--default', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`ProgressBar.Default.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-progressbar--default', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) - - test.describe('Progress Zero', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--progress-zero', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`ProgressBar.Progress Zero.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--progress-zero', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) - - test.describe('Progress Half', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--progress-half', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`ProgressBar.Progress Half.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--progress-half', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) - - test.describe('Progress Done', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--progress-done', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`ProgressBar.Progress Done.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--progress-done', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) +const stories = [ + { + title: 'Default', + id: 'components-progressbar--default', + }, + { + title: 'Progress Zero', + id: 'components-progressbar-features--progress-zero', + }, + { + title: 'Progress Half', + id: 'components-progressbar-features--progress-half', + }, + { + title: 'Progress Done', + id: 'components-progressbar-features--progress-done', + }, + { + title: 'Size Small', + id: 'components-progressbar-features--size-small', + }, + { + title: 'Size Large', + id: 'components-progressbar-features--size-large', + }, + { + title: 'Inline', + id: 'components-progressbar-features--inline', + }, + { + title: 'Color', + id: 'components-progressbar-features--color', + }, + { + title: 'Dev SX Props', + id: 'components-progressbar-dev--default', + }, +] as const - test.describe('Size Small', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--size-small', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`ProgressBar.Size Small.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--size-small', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) - - test.describe('Size Large', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--size-large', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`ProgressBar.Size Large.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--size-large', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) - - test.describe('Inline', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--inline', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`ProgressBar.Inline.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--inline', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) - - test.describe('Color', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--color', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`ProgressBar.Color.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-progressbar-features--color', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) +test.describe('ProgressBar', () => { + 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, + }, + }) + + // Default state + expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot( + `ProgressBar.${story.title}.${theme}.png`, + ) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: story.id, + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations() + }) + }) + } + }) + } }) diff --git a/packages/react/src/ProgressBar/ProgressBar.dev.stories.tsx b/packages/react/src/ProgressBar/ProgressBar.dev.stories.tsx new file mode 100644 index 00000000000..4d3afa7d319 --- /dev/null +++ b/packages/react/src/ProgressBar/ProgressBar.dev.stories.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import {ProgressBar} from '.' + +export default { + title: 'Components/ProgressBar/Dev', + component: ProgressBar, +} + +export const Default = () => ( + +) diff --git a/packages/react/src/ProgressBar/ProgressBar.module.css b/packages/react/src/ProgressBar/ProgressBar.module.css new file mode 100644 index 00000000000..e9f3badcd38 --- /dev/null +++ b/packages/react/src/ProgressBar/ProgressBar.module.css @@ -0,0 +1,50 @@ +@keyframes shimmer { + from { + mask-position: 200%; + } + + to { + mask-position: 0%; + } +} + +.ProgressBarItem { + width: var(--progress-width); + /* stylelint-disable-next-line primer/colors */ + background-color: var(--progress-bg); + + @media (prefers-reduced-motion: no-preference) { + &[data-animated='true'] { + mask-image: linear-gradient(75deg, #000 30%, rgba(0, 0, 0, 0.65) 80%); + mask-size: 200%; + animation-name: shimmer; + animation-duration: 1s; + animation-iteration-count: infinite; + } + } +} + +.ProgressBarContainer { + display: flex; + overflow: hidden; + /* stylelint-disable-next-line primer/colors */ + background-color: var(--borderColor-default); + border-radius: var(--borderRadius-small); + gap: 2px; + + &:where([data-progress-display='inline']) { + display: inline-flex; + } + + &:where([data-progress-bar-size='default']) { + height: 8px; + } + + &:where([data-progress-bar-size='small']) { + height: 5px; + } + + &:where([data-progress-bar-size='large']) { + height: 10px; + } +} diff --git a/packages/react/src/ProgressBar/ProgressBar.tsx b/packages/react/src/ProgressBar/ProgressBar.tsx index 1a49d5a828a..69c048bcc09 100644 --- a/packages/react/src/ProgressBar/ProgressBar.tsx +++ b/packages/react/src/ProgressBar/ProgressBar.tsx @@ -5,8 +5,13 @@ import {width} from 'styled-system' import {get} from '../constants' import type {SxProp} from '../sx' import sx from '../sx' +import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent' +import {clsx} from 'clsx' +import classes from './ProgressBar.module.css' +import {useFeatureFlag} from '../FeatureFlags' type ProgressProp = { + className?: string progress?: string | number bg?: string } @@ -16,22 +21,28 @@ const shimmer = keyframes` to { mask-position: 0%; } ` -const ProgressItem = styled.span` - width: ${props => (props.progress ? `${props.progress}%` : 0)}; - background-color: ${props => get(`colors.${props.bg || 'success.emphasis'}`)}; - - @media (prefers-reduced-motion: no-preference) { - &[data-animated='true'] { - mask-image: linear-gradient(75deg, #000 30%, rgba(0, 0, 0, 0.65) 80%); - mask-size: 200%; - animation: ${shimmer}; - animation-duration: 1s; - animation-iteration-count: infinite; +const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team' + +const ProgressItem = toggleStyledComponent( + CSS_MODULES_FEATURE_FLAG, + 'span', + styled.span` + width: ${props => (props.progress ? `${props.progress}%` : 0)}; + background-color: ${props => get(`colors.${props.bg || 'success.emphasis'}`)}; + + @media (prefers-reduced-motion: no-preference) { + &[data-animated='true'] { + mask-image: linear-gradient(75deg, #000 30%, rgba(0, 0, 0, 0.65) 80%); + mask-size: 200%; + animation: ${shimmer}; + animation-duration: 1s; + animation-iteration-count: infinite; + } } - } - ${sx}; -` + ${sx}; + `, +) const sizeMap = { small: '5px', @@ -46,22 +57,37 @@ type StyledProgressContainerProps = { } & WidthProps & SxProp -const ProgressContainer = styled.span` - display: ${props => (props.inline ? 'inline-flex' : 'flex')}; - overflow: hidden; - background-color: ${get('colors.border.default')}; - border-radius: ${get('radii.1')}; - height: ${props => sizeMap[props.barSize || 'default']}; - gap: 2px; - ${width} - ${sx}; -` +const ProgressContainer = toggleStyledComponent( + CSS_MODULES_FEATURE_FLAG, + 'span', + styled.span` + display: ${props => (props.inline ? 'inline-flex' : 'flex')}; + overflow: hidden; + background-color: ${get('colors.border.default')}; + border-radius: ${get('radii.1')}; + height: ${props => sizeMap[props.barSize || 'default']}; + gap: 2px; + ${width} + ${sx}; + `, +) -export type ProgressBarItems = React.HTMLAttributes & {'aria-label'?: string} & ProgressProp & SxProp +export type ProgressBarItems = React.HTMLAttributes & { + 'aria-label'?: string + className?: string +} & ProgressProp & + SxProp export const Item = forwardRef( ( - {progress, 'aria-label': ariaLabel, 'aria-valuenow': ariaValueNow, 'aria-valuetext': ariaValueText, ...rest}, + { + progress, + 'aria-label': ariaLabel, + 'aria-valuenow': ariaValueNow, + 'aria-valuetext': ariaValueText, + className, + ...rest + }, forwardRef, ) => { const progressAsNumber = typeof progress === 'string' ? parseInt(progress, 10) : progress @@ -74,13 +100,25 @@ export const Item = forwardRef( 'aria-valuetext': ariaValueText, } + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + + const progressBarWidth = '--progress-width' + const progressBarBg = '--progress-bg' + const styles: {[key: string]: string} = {} + + const bgType = rest.bg && rest.bg.split('.') + styles[progressBarWidth] = progress ? `${progress}%` : '0%' + styles[progressBarBg] = (bgType && `var(--bgColor-${bgType[0]}-${bgType[1]})`) || 'var(--bgColor-success-emphasis)' + return ( ) @@ -89,7 +127,10 @@ export const Item = forwardRef( Item.displayName = 'ProgressBar.Item' -export type ProgressBarProps = React.HTMLAttributes & {bg?: string} & StyledProgressContainerProps & +export type ProgressBarProps = React.HTMLAttributes & { + bg?: string + className?: string +} & StyledProgressContainerProps & ProgressProp export const ProgressBar = forwardRef( @@ -103,6 +144,7 @@ export const ProgressBar = forwardRef( 'aria-label': ariaLabel, 'aria-valuenow': ariaValueNow, 'aria-valuetext': ariaValueText, + className, ...rest }: ProgressBarProps, forwardRef, @@ -114,9 +156,19 @@ export const ProgressBar = forwardRef( // Get the number of non-empty nodes passed as children, this will exclude // booleans, null, and undefined const validChildren = React.Children.toArray(children).length + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + + const cssModulesProps = !enabled + ? {barSize} + : {'data-progress-display': rest.inline ? 'inline' : 'block', 'data-progress-bar-size': barSize} return ( - + {validChildren ? ( children ) : (