diff --git a/.changeset/olive-donkeys-exercise.md b/.changeset/olive-donkeys-exercise.md new file mode 100644 index 00000000000..638d5ac14ba --- /dev/null +++ b/.changeset/olive-donkeys-exercise.md @@ -0,0 +1,5 @@ +--- +"@primer/react": major +--- + +Refactor ButtonBase component to use CSS modules behine flag diff --git a/.vscode/settings.json b/.vscode/settings.json index eb183dfaed1..f92d3119fd1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,13 @@ "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[postcss]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.stylelint": "explicit" + } + }, "json.schemas": [ { "fileMatch": ["*.docs.json"], diff --git a/e2e/components/IconButton.test.ts b/e2e/components/IconButton.test.ts index 86a6b3d1c26..2fa278e275e 100644 --- a/e2e/components/IconButton.test.ts +++ b/e2e/components/IconButton.test.ts @@ -3,372 +3,442 @@ import {visit} from '../test-helpers/storybook' import {themes} from '../test-helpers/themes' test.describe('IconButton', () => { - test.describe('Playground', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-iconbutton--playground', - globals: { - colorScheme: theme, - }, + for (const featureFlagOn of [true, false]) { + test.describe(`Feature flag: ${featureFlagOn ? 'on' : 'off'}`, () => { + test.describe('Playground', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-iconbutton--playground', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`IconButton.Playground.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-iconbutton--playground', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`IconButton.Playground.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-iconbutton--playground', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + } }) - } - }) - - test.describe('Danger', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--danger', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`IconButton.Danger.${theme}.png`) - }) - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--danger', - globals: { - colorScheme: theme, - }, + test.describe('Danger', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--danger', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`IconButton.Danger.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--danger', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + } }) - } - }) - - test.describe('Default', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-iconbutton--default', - globals: { - colorScheme: theme, - }, - }) - // Default state - expect(await page.screenshot()).toMatchSnapshot(`IconButton.Default.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-iconbutton--default', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, + test.describe('Default', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-iconbutton--default', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`IconButton.Default.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-iconbutton--default', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) }) - }) + } }) - } - }) - - test.describe('Disabled', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--disabled', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`IconButton.Disabled.${theme}.png`) - }) - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--disabled', - globals: { - colorScheme: theme, - }, + test.describe('Disabled', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--disabled', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`IconButton.Disabled.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--disabled', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + } }) - } - }) - - test.describe('Invisible', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--invisible', - globals: { - colorScheme: theme, - }, - }) - // Default state - expect(await page.screenshot()).toMatchSnapshot(`IconButton.Invisible.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--invisible', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, + test.describe('Invisible', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--invisible', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`IconButton.Invisible.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--invisible', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) }) - }) + } }) - } - }) - - test.describe('Large', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--large', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`IconButton.Large.${theme}.png`) - }) - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--large', - globals: { - colorScheme: theme, - }, + test.describe('Large', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--large', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`IconButton.Large.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--large', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + } }) - } - }) - - test.describe('Medium', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--medium', - globals: { - colorScheme: theme, - }, - }) - // Default state - expect(await page.screenshot()).toMatchSnapshot(`IconButton.Medium.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--medium', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, + test.describe('Medium', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--medium', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`IconButton.Medium.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--medium', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) }) - }) + } }) - } - }) - - test.describe('Primary', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--primary', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`IconButton.Primary.${theme}.png`) - }) - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--primary', - globals: { - colorScheme: theme, - }, + test.describe('Primary', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--primary', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`IconButton.Primary.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--primary', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + } }) - } - }) - - test.describe('Small', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--small', - globals: { - colorScheme: theme, - }, - }) - // Default state - expect(await page.screenshot()).toMatchSnapshot(`IconButton.Small.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--small', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, + test.describe('Small', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--small', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`IconButton.Small.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--small', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) }) - }) + } }) - } - }) - test.describe('Keyshortcuts', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--keyshortcuts', - globals: { - colorScheme: theme, - }, + test.describe('Keyshortcuts', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--keyshortcuts', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + await page.keyboard.press('Tab') // focus on icon button + expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot( + `IconButton.Keyshortcuts.${theme}.png`, + ) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--keyshortcuts', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await page.keyboard.press('Tab') // focus on icon button + await expect(page).toHaveNoViolations() + }) }) - - // Default state - await page.keyboard.press('Tab') // focus on icon button - expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot( - `IconButton.Keyshortcuts.${theme}.png`, - ) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--keyshortcuts', - globals: { - colorScheme: theme, - }, - }) - await page.keyboard.press('Tab') // focus on icon button - await expect(page).toHaveNoViolations() - }) + } }) - } - }) - - test.describe('Keyshortcuts on Description', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--keyshortcuts-on-description', - globals: { - colorScheme: theme, - }, - }) - // Default state - await page.keyboard.press('Tab') // focus on icon button - expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot( - `IconButton.Keyshortcuts on Description.${theme}.png`, - ) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-iconbutton-features--keyshortcuts-on-description', - globals: { - colorScheme: theme, - }, + test.describe('Keyshortcuts on Description', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--keyshortcuts-on-description', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + await page.keyboard.press('Tab') // focus on icon button + expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot( + `IconButton.Keyshortcuts on Description.${theme}.png`, + ) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-iconbutton-features--keyshortcuts-on-description', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await page.keyboard.press('Tab') // focus on icon button + await expect(page).toHaveNoViolations() + }) }) - await page.keyboard.press('Tab') // focus on icon button - await expect(page).toHaveNoViolations() - }) + } }) - } - }) + }) + } }) diff --git a/e2e/components/LinkButton.test.ts b/e2e/components/LinkButton.test.ts index 98a92c22704..962761f750f 100644 --- a/e2e/components/LinkButton.test.ts +++ b/e2e/components/LinkButton.test.ts @@ -3,377 +3,447 @@ import {visit} from '../test-helpers/storybook' import {themes} from '../test-helpers/themes' test.describe('LinkButton', () => { - test.describe('Playground', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-linkbutton--playground', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Playground.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-linkbutton--playground', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + for (const featureFlagOn of [true, false]) { + test.describe(`Feature flag: ${featureFlagOn ? 'on' : 'off'}`, () => { + test.describe('Playground', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-linkbutton--playground', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Playground.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-linkbutton--playground', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } }) - } - }) - - test.describe('Danger', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--danger', - globals: { - colorScheme: theme, - }, - }) - // Default state - expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Danger.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--danger', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + test.describe('Danger', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--danger', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Danger.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--danger', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } }) - } - }) - - test.describe('Default', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-linkbutton--default', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Default.${theme}.png`) - }) - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-linkbutton--default', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + test.describe('Default', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-linkbutton--default', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Default.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-linkbutton--default', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } }) - } - }) - - test.describe('Invisible', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--invisible', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Invisible.${theme}.png`) - }) - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--invisible', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + test.describe('Invisible', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--invisible', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Invisible.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--invisible', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } }) - } - }) - - test.describe('Large', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--large', - globals: { - colorScheme: theme, - }, - }) - // Default state - expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Large.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--large', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + test.describe('Large', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--large', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Large.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--large', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } }) - } - }) - - test.describe('Leading Visual', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--leading-visual', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Leading Visual.${theme}.png`) - }) - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--leading-visual', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + test.describe('Leading Visual', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--leading-visual', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Leading Visual.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--leading-visual', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } }) - } - }) - - test.describe('Medium', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--medium', - globals: { - colorScheme: theme, - }, - }) - // Default state - expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Medium.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--medium', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + test.describe('Medium', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--medium', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Medium.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--medium', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } }) - } - }) - - test.describe('Primary', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--primary', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Primary.${theme}.png`) - }) - test.fixme('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--primary', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + test.describe('Primary', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--primary', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Primary.${theme}.png`) + }) + + test.fixme('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--primary', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } }) - } - }) - - test.describe('Small', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--small', - globals: { - colorScheme: theme, - }, - }) - // Default state - expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Small.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--small', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + test.describe('Small', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--small', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Small.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--small', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } }) - } - }) - - test.describe('Trailing Visual', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--trailing-visual', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Trailing Visual.${theme}.png`) - }) - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--trailing-visual', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + test.describe('Trailing Visual', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--trailing-visual', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`LinkButton.Trailing Visual.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--trailing-visual', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } }) - } - }) - - test.describe('With React Router', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--with-react-router', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`LinkButton.With React Router.${theme}.png`) - }) - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-linkbutton-features--with-react-router', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations({ - rules: { - 'color-contrast': { - enabled: theme !== 'dark_dimmed', - }, - }, - }) - }) + test.describe('With React Router', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--with-react-router', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`LinkButton.With React Router.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-linkbutton-features--with-react-router', + globals: { + colorScheme: theme, + featureFlags: { + primer_react_css_modules_team: featureFlagOn, + }, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } }) - } - }) + }) + } }) diff --git a/packages/react/.storybook/storybook.css b/packages/react/.storybook/storybook.css index b495d3c6b23..21e5d2d88b4 100644 --- a/packages/react/.storybook/storybook.css +++ b/packages/react/.storybook/storybook.css @@ -54,3 +54,7 @@ font: var(--text-caption-shorthand); margin: 0; } + +.testCustomClassname { + color: var(--fgColor-sponsors); +} diff --git a/packages/react/src/Button/ButtonBase.module.css b/packages/react/src/Button/ButtonBase.module.css new file mode 100644 index 00000000000..7ce72d8adcd --- /dev/null +++ b/packages/react/src/Button/ButtonBase.module.css @@ -0,0 +1,501 @@ +/* Base styles */ +:where(.ButtonBase) { + display: flex; + min-width: max-content; + height: var(--control-medium-size); + /* stylelint-disable-next-line primer/spacing */ + padding: 0 var(--control-medium-paddingInline-normal); + font-family: inherit; + font-size: var(--text-body-size-medium); + font-weight: var(--base-text-weight-medium); + color: var(--button-default-fgColor-rest); + text-align: center; + text-decoration: none; + cursor: pointer; + user-select: none; + background-color: transparent; + border: var(--borderWidth-thin) solid; + border-color: var(--button-default-borderColor-rest); + border-radius: var(--borderRadius-medium); + transition: 80ms cubic-bezier(0.65, 0, 0.35, 1); + transition-property: color, fill, background-color, border-color; + appearance: none; + align-items: center; + justify-content: space-between; + gap: var(--base-size-8); + + &:hover { + transition-duration: 80ms; + } + + &:focus-visible { + outline: 2px solid var(--focus-outlineColor); + outline-offset: -2px; + box-shadow: none; + } + + &:active { + transition: none; + } + + &:disabled { + cursor: not-allowed; + box-shadow: none; + + & .Visual, + & .CounterLabel { + color: inherit; + } + } + + @media (forced-colors: active) { + &:focus { + outline: solid 1px transparent; + } + } + + /* Visuals */ + & .Visual { + display: flex; + color: var(--fgColor-muted); + pointer-events: none; + } + + /* mostly for CounterLabel */ + & .VisualWrap { + display: flex; + pointer-events: none; + } + + /* IconButton */ + + &:where(.IconButton) { + display: inline-grid; + width: var(--control-medium-size); + min-width: var(--control-medium-size); + /* stylelint-disable-next-line primer/spacing */ + padding: unset; + place-content: center; + + &:where([data-size='small']) { + width: var(--control-small-size); + min-width: var(--control-small-size); + } + + &:where([data-size='large']) { + width: var(--control-large-size); + min-width: var(--control-large-size); + } + } + + /* LinkButton */ + + &[href] { + display: inline-flex; + + &:hover { + text-decoration: none; + } + } + + /* Button layout */ + + & .ButtonContent { + flex: 1 0 auto; + display: grid; + grid-template-areas: 'leadingVisual text trailingVisual'; + grid-template-columns: min-content minmax(0, auto) min-content; + align-items: center; + align-content: center; + + & > :not(:last-child) { + margin-right: var(--base-size-8); + } + + /* Content alignment */ + + &:where([data-align='center']) { + justify-content: center; + } + + &:where([data-align='start']) { + justify-content: flex-start; + } + } + + & :where([data-component='leadingVisual']) { + grid-area: leadingVisual; + } + + & .Label { + line-height: 1.4285714; /* temporary until we use Text component with --text-body-lineHeight-medium */ + white-space: nowrap; + grid-area: text; + } + + & :where([data-component='trailingVisual']) { + grid-area: trailingVisual; + } + + & :where([data-component='trailingAction']) { + margin-right: calc(var(--base-size-4) * -1); + } + + /* Size */ + + &:where([data-size='small']) { + height: var(--control-small-size); + /* stylelint-disable-next-line primer/spacing */ + padding: 0 var(--control-small-paddingInline-condensed); + gap: var(--control-small-gap); + font-size: var(--text-body-size-small); + + & .ButtonContent > :not(:last-child) { + /* stylelint-disable-next-line primer/spacing */ + margin-right: var(--control-small-gap); + } + + & .Label { + line-height: 1.6666667; /* temporary until we use Text component with --text-body-lineHeight-small */ + } + } + + &:where([data-size='large']) { + height: var(--control-large-size); + /* stylelint-disable-next-line primer/spacing */ + padding: 0 var(--control-large-paddingInline-spacious); + gap: var(--control-large-gap); + + & .ButtonContent > :not(:last-child) { + /* stylelint-disable-next-line primer/spacing */ + margin-right: var(--control-large-gap); + } + } + + /* Full width */ + + &:where([data-block='block']) { + width: 100%; + } + + /* Wrap label text */ + + &:where([data-label-wrap='true']) { + min-width: fit-content; + height: unset; + min-height: var(--control-medium-size); + + & .ButtonContent { + flex: 1 1 auto; + align-self: stretch; + /* stylelint-disable-next-line primer/spacing */ + padding-block: calc(var(--control-medium-paddingBlock) - var(--base-size-2)); + } + + & .Label { + word-break: break-word; + white-space: unset; + } + + &:where([data-size='small']) { + height: unset; + min-height: var(--control-small-size); + + & .ButtonContent { + /* stylelint-disable-next-line primer/spacing */ + padding-block: calc(var(--control-small-paddingBlock) - var(--base-size-2)); + } + } + + &:where([data-size='large']) { + height: unset; + min-height: var(--control-large-size); + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--control-large-paddingInline-spacious); + + & .ButtonContent { + /* stylelint-disable-next-line primer/spacing */ + padding-block: calc(var(--control-large-paddingBlock) - var(--base-size-2)); + } + } + } + + /* Loading */ + + /* only hide label if there's no leading/trailing visuals + * move spinner to label spot if not leading/trailing visuals + */ + + &:where([data-loading='true']) { + & + .loadingSpinner:not( + [data-component='leadingVisual'], + [data-component='trailingVisual'], + [data-component='trailingAction'] + ) { + grid-area: text; + margin-right: 0 !important; + place-self: center; + + & + .Label { + visibility: hidden; + } + } + } + + /* Default Variant */ + + &:where([data-variant='default']) { + color: var(--button-default-fgColor-rest); + background-color: var(--button-default-bgColor-rest); + box-shadow: var(--button-default-shadow-resting); + + &:hover { + background-color: var(--button-default-bgColor-hover); + border-color: var(--button-default-borderColor-hover); + } + + &:active { + background-color: var(--button-default-bgColor-active); + border-color: var(--button-default-borderColor-active); + } + + &:disabled { + color: var(--control-fgColor-disabled); + background-color: var(--button-default-bgColor-disabled); + border-color: var(--button-default-borderColor-disabled); + box-shadow: none; + } + + &[aria-expanded='true'] { + background-color: var(--button-default-bgColor-active); + border-color: var(--button-default-borderColor-active); + } + + & .CounterLabel { + background-color: var(--buttonCounter-default-bgColor-rest) !important; /* temporarily override our own sx prop */ + } + + &:where(.IconButton) { + color: var(--fgColor-muted); + } + } + + /* Primary variant */ + + &:where([data-variant='primary']) { + color: var(--button-primary-fgColor-rest); + background-color: var(--button-primary-bgColor-rest); + border-color: var(--button-primary-borderColor-rest); + box-shadow: var(--shadow-resting-small); + + &:hover { + background-color: var(--button-primary-bgColor-hover); + border-color: var(--button-primary-borderColor-hover); + } + + &:focus-visible { + outline: 2px solid var(--focus-outlineColor); + outline-offset: -2px; + box-shadow: inset 0 0 0 3px var(--fgColor-onEmphasis); + } + + &:active { + background-color: var(--button-primary-bgColor-active); + box-shadow: var(--button-primary-shadow-selected); + } + + &:disabled { + color: var(--button-primary-fgColor-disabled); + background-color: var(--button-primary-bgColor-disabled); + border-color: var(--button-primary-borderColor-disabled); + box-shadow: none; + } + + &[aria-expanded='true'] { + background-color: var(--button-primary-bgColor-active); + box-shadow: var(--button-primary-shadow-selected); + } + + & .CounterLabel { + color: var(--button-primary-fgColor-rest) !important; /* temporarily override our own sx prop */ + background-color: var(--buttonCounter-primary-bgColor-rest) !important; /* temporarily override our own sx prop */ + } + + /* temporarily using the fgColor to match legacy and reduce visual changes- will eventually be iconColor */ + & .Visual { + color: var(--button-primary-fgColor-rest); + } + } + + /* Danger variant */ + + &:where([data-variant='danger']) { + color: var(--button-danger-fgColor-rest); + background-color: var(--button-danger-bgColor-rest); + box-shadow: var(--button-default-shadow-resting); + + &:hover { + color: var(--button-danger-fgColor-hover); + background-color: var(--button-danger-bgColor-hover); + border-color: var(--button-danger-borderColor-hover); + box-shadow: var(--shadow-resting-small); + + & .CounterLabel { + color: var(--buttonCounter-danger-fgColor-hover) !important; /* temporarily override our own sx prop */ + background-color: var(--buttonCounter-danger-bgColor-hover) !important; + } + + & .Visual { + color: var(--button-danger-iconColor-hover); + } + } + + &:active { + color: var(--button-danger-fgColor-active); + background-color: var(--button-danger-bgColor-active); + border-color: var(--button-danger-borderColor-active); + box-shadow: var(--button-danger-shadow-selected); + + & .CounterLabel { + color: var(--buttonCounter-danger-fgColor-hover) !important; /* temporarily override our own sx prop */ + background-color: var(--buttonCounter-danger-bgColor-hover) !important; + } + } + + &:disabled { + color: var(--button-danger-fgColor-disabled); + background-color: var(--button-danger-bgColor-disabled); + border-color: var(--button-default-borderColor-disabled); + box-shadow: none; + + & .CounterLabel { + color: var(--buttonCounter-danger-fgColor-disabled) !important; /* temporarily override our own sx prop */ + background-color: var(--buttonCounter-danger-bgColor-disabled) !important; + } + } + + &[aria-expanded='true'] { + color: var(--button-danger-fgColor-active); + background-color: var(--button-danger-bgColor-active); + border-color: var(--button-danger-borderColor-active); + box-shadow: var(--button-danger-shadow-selected); + } + + & .CounterLabel { + color: var(--buttonCounter-danger-fgColor-rest) !important; /* temporarily override our own sx prop */ + background-color: var(--buttonCounter-danger-bgColor-rest) !important; + } + + & .Visual { + color: var(--button-danger-iconColor-rest); + } + } + + /* Invisible variant */ + + &:where([data-variant='invisible']) { + color: var(--button-default-fgColor-rest); + border-color: transparent; + box-shadow: none; + + &:hover { + background-color: var(--button-invisible-bgColor-hover); + + & .Visual { + color: var(--button-invisible-iconColor-hover); + } + } + + &:active { + background-color: var(--button-invisible-bgColor-active); + + & .Visual { + color: var(--button-invisible-iconColor-hover); + } + } + + &:disabled { + color: var(--button-invisible-fgColor-disabled); + background-color: var(--button-invisible-bgColor-disabled); + border-color: var(--button-invisible-borderColor-disabled); + box-shadow: none; + } + + &[aria-expanded='true'] { + background-color: var(--button-invisible-bgColor-active); + } + + & .Visual { + color: var(--button-invisible-iconColor-rest); + } + + & .CounterLabel { + background-color: var(--buttonCounter-invisible-bgColor-rest) !important; + } + + &:where(.IconButton) { + color: var(--button-invisible-iconColor-rest); + } + } + + /* Link variant */ + + &:where([data-variant='link']) { + display: inline-flex; + min-width: fit-content; + height: unset; + padding: 0; + font-size: inherit; + color: var(--fgColor-link); + text-align: left; + border: unset; + + &:hover:not(:disabled, [data-inactive]) { + text-decoration: underline; + } + + &:focus-visible, + &:focus { + outline-offset: 2px; + } + + &:disabled { + color: var(--control-fgColor-disabled); + background-color: transparent; + border-color: transparent; + } + + & .Label { + white-space: unset; + } + + &:where([data-inactive]) { + color: var(--button-inactive-fgColor); + background: transparent !important; + } + + & .Visual { + color: var(--fgColor-link); + } + } + + /* Inactive */ + + &:where([data-inactive]), + &:where([data-inactive]):hover { + color: var(--button-inactive-fgColor); + cursor: auto; + background-color: var(--button-inactive-bgColor); + border-color: var(--button-inactive-bgColor); + + & .Visual, + & .CounterLabel { + color: inherit !important; + } + } +} + +.ConditionalWrapper { + display: block; +} diff --git a/packages/react/src/Button/ButtonBase.tsx b/packages/react/src/Button/ButtonBase.tsx index 8b66fb72394..8f1bc8b856c 100644 --- a/packages/react/src/Button/ButtonBase.tsx +++ b/packages/react/src/Button/ButtonBase.tsx @@ -16,6 +16,9 @@ import CounterLabel from '../CounterLabel' import {useId} from '../hooks' import {ConditionalWrapper} from '../internal/components/ConditionalWrapper' import {AriaStatus} from '../live-region' +import {clsx} from 'clsx' +import classes from './ButtonBase.module.css' +import {useFeatureFlag} from '../FeatureFlags' const iconWrapStyles = { display: 'flex', @@ -28,6 +31,15 @@ const renderVisual = (Visual: React.ElementType, loading: boolean, visualName: s ) +const renderModuleVisual = (Visual: React.ElementType, loading: boolean, visualName: string, counterLabel: boolean) => ( + + {loading ? : } + +) + const ButtonBase = forwardRef( ({children, as: Component = 'button', sx: sxProp = defaultSxProp, ...props}, forwardedRef): JSX.Element => { const { @@ -48,9 +60,11 @@ const ButtonBase = forwardRef( inactive, onClick, labelWrap, + className, ...rest } = props + const enabled = useFeatureFlag('primer_react_css_modules_team') const innerRef = React.useRef(null) useRefObjectAsForwardedRef(forwardedRef, innerRef) @@ -84,6 +98,239 @@ const ButtonBase = forwardRef( }, [innerRef]) } + if (enabled) { + if (sxProp !== defaultSxProp) { + return ( + + Boolean(descriptionID)) + .join(' ')} + // aria-labelledby is needed because the accessible name becomes unset when the button is in a loading state. + // We only set it when the button is in a loading state because it will supercede the aria-label when the screen + // reader announces the button name. + aria-labelledby={ + loading + ? [`${uuid}-label`, ariaLabelledBy].filter(labelID => Boolean(labelID)).join(' ') + : ariaLabelledBy + } + id={id} + onClick={loading ? undefined : onClick} + > + {Icon ? ( + loading ? ( + + ) : ( + + ) + ) : ( + <> + + { + /* If there are no leading/trailing visuals/actions to replace with a loading spinner, + render a loading spiner in place of the button content. */ + loading && + !LeadingVisual && + !TrailingVisual && + !TrailingAction && + renderModuleVisual(Spinner, loading, 'loadingSpinner', false) + } + { + /* Render a leading visual unless the button is in a loading state. + Then replace the leading visual with a loading spinner. */ + LeadingVisual && renderModuleVisual(LeadingVisual, Boolean(loading), 'leadingVisual', false) + } + {children && ( + + {children} + + )} + { + /* If there is a count, render a counter label unless there is a trailing visual. + Then render the counter label as a trailing visual. + Replace the counter label or the trailing visual with a loading spinner if: + - the button is in a loading state + - there is no leading visual to replace with a loading spinner + */ + count !== undefined && !TrailingVisual + ? renderModuleVisual( + () => ( + + {count} + + ), + Boolean(loading) && !LeadingVisual, + 'trailingVisual', + true, + ) + : TrailingVisual + ? renderModuleVisual( + TrailingVisual, + Boolean(loading) && !LeadingVisual, + 'trailingVisual', + false, + ) + : null + } + + { + /* If there is a trailing action, render it unless the button is in a loading state + and there is no leading or trailing visual to replace with a loading spinner. */ + TrailingAction && + renderModuleVisual( + TrailingAction, + Boolean(loading) && !LeadingVisual && !TrailingVisual, + 'trailingAction', + false, + ) + } + + )} + + {loading && ( + + {loadingAnnouncement} + + )} + + ) + } + return ( + + Boolean(descriptionID)) + .join(' ')} + // aria-labelledby is needed because the accessible name becomes unset when the button is in a loading state. + // We only set it when the button is in a loading state because it will supercede the aria-label when the screen + // reader announces the button name. + aria-labelledby={ + loading ? [`${uuid}-label`, ariaLabelledBy].filter(labelID => Boolean(labelID)).join(' ') : ariaLabelledBy + } + id={id} + // @ts-ignore temporary disable as we migrate to css modules, until we remove PolymorphicForwardRefComponent + onClick={loading ? undefined : onClick} + > + {Icon ? ( + loading ? ( + + ) : ( + + ) + ) : ( + <> + + { + /* If there are no leading/trailing visuals/actions to replace with a loading spinner, + render a loading spiner in place of the button content. */ + loading && + !LeadingVisual && + !TrailingVisual && + !TrailingAction && + renderModuleVisual(Spinner, loading, 'loadingSpinner', false) + } + { + /* Render a leading visual unless the button is in a loading state. + Then replace the leading visual with a loading spinner. */ + LeadingVisual && renderModuleVisual(LeadingVisual, Boolean(loading), 'leadingVisual', false) + } + {children && ( + + {children} + + )} + { + /* If there is a count, render a counter label unless there is a trailing visual. + Then render the counter label as a trailing visual. + Replace the counter label or the trailing visual with a loading spinner if: + - the button is in a loading state + - there is no leading visual to replace with a loading spinner + */ + count !== undefined && !TrailingVisual + ? renderModuleVisual( + () => ( + + {count} + + ), + Boolean(loading) && !LeadingVisual, + 'trailingVisual', + true, + ) + : TrailingVisual + ? renderModuleVisual(TrailingVisual, Boolean(loading) && !LeadingVisual, 'trailingVisual', false) + : null + } + + { + /* If there is a trailing action, render it unless the button is in a loading state + and there is no leading or trailing visual to replace with a loading spinner. */ + TrailingAction && + renderModuleVisual( + TrailingAction, + Boolean(loading) && !LeadingVisual && !TrailingVisual, + 'trailingAction', + false, + ) + } + + )} + + {loading && ( + + {loadingAnnouncement} + + )} + + ) + } + return ( { expect(tooltipEl).toBeInTheDocument() expect(triggerEl.getAttribute('aria-describedby')).toEqual(expect.stringContaining(tooltipEl.id)) }) + + describe('with primer_react_css_modules_staff enabled', () => { + it('iconbutton should support custom `className` along with default classnames', () => { + const {container} = render( + + + , + ) + expect(container.firstChild).toHaveClass('IconButton') + }) + + it('button should support custom `className` along with default classnames', () => { + const {container} = render( + + + , + ) + expect(container.firstChild).toHaveClass('ButtonBase') + }) + + it('linkbutton should support custom `className` along with default classnames', () => { + const {container} = render( + + Hello + , + ) + expect(container.firstChild).toHaveClass('ButtonBase') + }) + }) }) diff --git a/packages/react/src/Button/__tests__/__snapshots__/Button.test.tsx.snap b/packages/react/src/Button/__tests__/__snapshots__/Button.test.tsx.snap index f070f6eb26c..3d13c559732 100644 --- a/packages/react/src/Button/__tests__/__snapshots__/Button.test.tsx.snap +++ b/packages/react/src/Button/__tests__/__snapshots__/Button.test.tsx.snap @@ -128,7 +128,7 @@ exports[`Button respects block prop 1`] = ` } .c0[data-size="small"] [data-component="text"] { - line-height: calc(20 / 12); + line-height: 1.6666667; } .c0[data-size="small"] [data-component=ButtonCounter] { @@ -221,7 +221,7 @@ exports[`Button respects block prop 1`] = ` .c0 [data-component="text"] { grid-area: text; - line-height: calc(20/14); + line-height: 1.4285714; white-space: nowrap; } @@ -445,7 +445,7 @@ exports[`Button respects the alignContent prop 1`] = ` } .c0[data-size="small"] [data-component="text"] { - line-height: calc(20 / 12); + line-height: 1.6666667; } .c0[data-size="small"] [data-component=ButtonCounter] { @@ -538,7 +538,7 @@ exports[`Button respects the alignContent prop 1`] = ` .c0 [data-component="text"] { grid-area: text; - line-height: calc(20/14); + line-height: 1.4285714; white-space: nowrap; } @@ -761,7 +761,7 @@ exports[`Button respects the large size prop 1`] = ` } .c0[data-size="small"] [data-component="text"] { - line-height: calc(20 / 12); + line-height: 1.6666667; } .c0[data-size="small"] [data-component=ButtonCounter] { @@ -854,7 +854,7 @@ exports[`Button respects the large size prop 1`] = ` .c0 [data-component="text"] { grid-area: text; - line-height: calc(20/14); + line-height: 1.4285714; white-space: nowrap; } @@ -1078,7 +1078,7 @@ exports[`Button respects the small size prop 1`] = ` } .c0[data-size="small"] [data-component="text"] { - line-height: calc(20 / 12); + line-height: 1.6666667; } .c0[data-size="small"] [data-component=ButtonCounter] { @@ -1171,7 +1171,7 @@ exports[`Button respects the small size prop 1`] = ` .c0 [data-component="text"] { grid-area: text; - line-height: calc(20/14); + line-height: 1.4285714; white-space: nowrap; } @@ -1397,7 +1397,7 @@ exports[`Button styles danger button appropriately 1`] = ` } .c0[data-size="small"] [data-component="text"] { - line-height: calc(20 / 12); + line-height: 1.6666667; } .c0[data-size="small"] [data-component=ButtonCounter] { @@ -1490,7 +1490,7 @@ exports[`Button styles danger button appropriately 1`] = ` .c0 [data-component="text"] { grid-area: text; - line-height: calc(20/14); + line-height: 1.4285714; white-space: nowrap; } @@ -1714,7 +1714,7 @@ exports[`Button styles invisible button appropriately 1`] = ` } .c0[data-size="small"] [data-component="text"] { - line-height: calc(20 / 12); + line-height: 1.6666667; } .c0[data-size="small"] [data-component=ButtonCounter] { @@ -1808,7 +1808,7 @@ exports[`Button styles invisible button appropriately 1`] = ` .c0 [data-component="text"] { grid-area: text; - line-height: calc(20/14); + line-height: 1.4285714; white-space: nowrap; } @@ -2041,7 +2041,7 @@ exports[`Button styles primary button appropriately 1`] = ` } .c0[data-size="small"] [data-component="text"] { - line-height: calc(20 / 12); + line-height: 1.6666667; } .c0[data-size="small"] [data-component=ButtonCounter] { @@ -2134,7 +2134,7 @@ exports[`Button styles primary button appropriately 1`] = ` .c0 [data-component="text"] { grid-area: text; - line-height: calc(20/14); + line-height: 1.4285714; white-space: nowrap; } diff --git a/packages/react/src/Button/styles.ts b/packages/react/src/Button/styles.ts index 0706510094e..dafe8a8050a 100644 --- a/packages/react/src/Button/styles.ts +++ b/packages/react/src/Button/styles.ts @@ -299,7 +299,7 @@ export const getBaseStyles = (theme?: Theme) => ({ fontSize: '0', '[data-component="text"]': { - lineHeight: 'calc(20 / 12)', + lineHeight: '1.6666667', }, '[data-component=ButtonCounter]': { @@ -385,7 +385,7 @@ export const getButtonStyles = (theme?: Theme) => { }, '[data-component="text"]': { gridArea: 'text', - lineHeight: 'calc(20/14)', + lineHeight: '1.4285714', whiteSpace: 'nowrap', }, '[data-component="trailingVisual"]': { diff --git a/packages/react/src/CounterLabel/CounterLabel.tsx b/packages/react/src/CounterLabel/CounterLabel.tsx index d90e08c3a4a..dad3e633847 100644 --- a/packages/react/src/CounterLabel/CounterLabel.tsx +++ b/packages/react/src/CounterLabel/CounterLabel.tsx @@ -9,15 +9,17 @@ import {defaultSxProp} from '../utils/defaultSxProp' export type CounterLabelProps = React.PropsWithChildren< HTMLAttributes & { scheme?: 'primary' | 'secondary' + className?: string } & SxProp > const CounterLabel = forwardRef( - ({scheme = 'secondary', sx = defaultSxProp, children, ...props}, forwardedRef) => { + ({scheme = 'secondary', sx = defaultSxProp, children, className, ...props}, forwardedRef) => { return ( <>