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 (
<>
(
{
display: 'inline-block',
diff --git a/packages/react/src/Heading/Heading.tsx b/packages/react/src/Heading/Heading.tsx
index 45e1fc4160d..58b0ccfbd05 100644
--- a/packages/react/src/Heading/Heading.tsx
+++ b/packages/react/src/Heading/Heading.tsx
@@ -65,7 +65,7 @@ const Heading = forwardRef(({as: Component = 'h2', className, variant, ...props}
className={clsx(className, classes.Heading)}
data-variant={variant}
{...props}
- // @ts-ignore shh
+ // @ts-ignore temporary disable as we migrate to css modules, until we remove PolymorphicForwardRefComponent
ref={innerRef}
/>
)
@@ -80,7 +80,7 @@ const Heading = forwardRef(({as: Component = 'h2', className, variant, ...props}
data-variant={variant}
sx={sx}
{...props}
- // @ts-ignore shh
+ // @ts-ignore temporary disable as we migrate to css modules, until we remove PolymorphicForwardRefComponent
ref={innerRef}
/>
)
diff --git a/packages/react/src/__tests__/__snapshots__/TextInput.test.tsx.snap b/packages/react/src/__tests__/__snapshots__/TextInput.test.tsx.snap
index 42f11ddbd64..0937b41790c 100644
--- a/packages/react/src/__tests__/__snapshots__/TextInput.test.tsx.snap
+++ b/packages/react/src/__tests__/__snapshots__/TextInput.test.tsx.snap
@@ -1790,7 +1790,7 @@ exports[`TextInput renders trailingAction icon button 1`] = `
}
.c4[data-size="small"] [data-component="text"] {
- line-height: calc(20 / 12);
+ line-height: 1.6666667;
}
.c4[data-size="small"] [data-component=ButtonCounter] {
@@ -1884,7 +1884,7 @@ exports[`TextInput renders trailingAction icon button 1`] = `
.c4 [data-component="text"] {
grid-area: text;
- line-height: calc(20/14);
+ line-height: 1.4285714;
white-space: nowrap;
}
@@ -2240,7 +2240,7 @@ exports[`TextInput renders trailingAction icon button 1`] = `