diff --git a/.changeset/soft-pans-punch.md b/.changeset/soft-pans-punch.md new file mode 100644 index 00000000000..e0a29220089 --- /dev/null +++ b/.changeset/soft-pans-punch.md @@ -0,0 +1,7 @@ +--- +'@primer/react': minor +--- + +Adds an 'inactive' state to buttons. An inactive button looks disabled and has aria-disabled, but it can still be clicked and focused. This was added to support buttons that are broken due to availability issues, but can't be removed from the page. + + diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-colorblind-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-colorblind-linux.png new file mode 100644 index 00000000000..d9dd6608e6a Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-dimmed-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-dimmed-linux.png new file mode 100644 index 00000000000..9a57ae787e0 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-high-contrast-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-high-contrast-linux.png new file mode 100644 index 00000000000..78a50c2bf71 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-linux.png new file mode 100644 index 00000000000..2ce2acffba0 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-tritanopia-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-tritanopia-linux.png new file mode 100644 index 00000000000..d9dd6608e6a Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-light-colorblind-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-light-colorblind-linux.png new file mode 100644 index 00000000000..06b96eae488 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-light-high-contrast-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-light-high-contrast-linux.png new file mode 100644 index 00000000000..761a31e4f20 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-light-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-light-linux.png new file mode 100644 index 00000000000..497c9262997 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-light-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-light-tritanopia-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-light-tritanopia-linux.png new file mode 100644 index 00000000000..06b96eae488 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Inactive-light-tritanopia-linux.png differ diff --git a/e2e/components/Button.test.ts b/e2e/components/Button.test.ts index d9a4755ab1a..cc8eee590ef 100644 --- a/e2e/components/Button.test.ts +++ b/e2e/components/Button.test.ts @@ -445,6 +445,40 @@ test.describe('Button', () => { } }) + test.describe('Inactive', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-button-features--inactive', + globals: { + colorScheme: theme, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`Button.Inactive.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-button-features--inactive', + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } + }) + test.describe('Dev: Invisible Variants', () => { for (const theme of themes) { test.describe(theme, () => { diff --git a/package-lock.json b/package-lock.json index b7f50fdd03b..6e31e4fb9de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@oddbird/popover-polyfill": "^0.3.1", "@primer/behaviors": "^1.5.1", "@primer/octicons-react": "^19.8.0", - "@primer/primitives": "^7.11.11", + "@primer/primitives": "7.15.3", "@react-aria/ssr": "^3.5.0", "@styled-system/css": "^5.1.5", "@styled-system/props": "^5.1.5", @@ -6031,9 +6031,9 @@ } }, "node_modules/@primer/primitives": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@primer/primitives/-/primitives-7.15.4.tgz", - "integrity": "sha512-471hL6pkcGuPS4G0urQ0TRZYo3ukAaVhUtvlsi1mZLofmX+EF+9iQL/iau06JfaQkm5NNP236+F7yyxXra9EjA==" + "version": "7.15.3", + "resolved": "https://registry.npmjs.org/@primer/primitives/-/primitives-7.15.3.tgz", + "integrity": "sha512-BFxFKwa0Bkr+esqbXU5Yt91z/58J2MPoW1cYtp0j2rUYus4lIZnczX7+ZYb7j4BqpfY/88q9Vn+BRwW/Sx4eIA==" }, "node_modules/@primer/view-components": { "version": "0.5.1", diff --git a/package.json b/package.json index 886af52e34a..49d6bb2f35c 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "@oddbird/popover-polyfill": "^0.3.1", "@primer/behaviors": "^1.5.1", "@primer/octicons-react": "^19.8.0", - "@primer/primitives": "^7.11.11", + "@primer/primitives": "7.15.3", "@react-aria/ssr": "^3.5.0", "@styled-system/css": "^5.1.5", "@styled-system/props": "^5.1.5", diff --git a/src/Button/Button.docs.json b/src/Button/Button.docs.json index e6f74974f5b..e2ab4c6b3d1 100644 --- a/src/Button/Button.docs.json +++ b/src/Button/Button.docs.json @@ -50,6 +50,11 @@ "type": "React.ElementType", "description": "A visual to display after the button text." }, + { + "name": "inactive", + "type": "boolean", + "description": "Whether the button looks visually disabled, but can still accept all the same interactions as an enabled button.\n This is intended to be used when a system error such as an outage prevents the button from performing its usual action.\n Inactive styles are slightly different from disabled styles because inactive buttons need to have an accessible color contrast ratio. This is because inactive buttons can have tooltips or perform an action such as opening a dialog explaining why it's inactive.\n If both `disabled` and `inactive` are true, `disabled` takes precedence." + }, { "name": "as", "type": "React.ElementType", diff --git a/src/Button/Button.features.stories.tsx b/src/Button/Button.features.stories.tsx index 96197d0692e..f923a02e616 100644 --- a/src/Button/Button.features.stories.tsx +++ b/src/Button/Button.features.stories.tsx @@ -1,6 +1,8 @@ import {EyeIcon, TriangleDownIcon, HeartIcon} from '@primer/octicons-react' import React, {useState} from 'react' import {Button} from '.' +import {Tooltip} from '../drafts' +import {ToggleSwitch, Text, Box, Dialog} from '../' export default { title: 'Components/Button/Features', @@ -63,8 +65,61 @@ export const Block = () => export const Disabled = () => +export const Inactive = () => ( + +) + export const Small = () => export const Medium = () => export const Large = () => + +const InactiveButtonHover = () => + +export const InactiveButtonWithTooltip = () => { + const [isOn, setIsOn] = React.useState(false) + const onClick = React.useCallback(() => { + setIsOn(!isOn) + }, [setIsOn, isOn]) + + return ( + + + Simulate outage + + + {isOn ? ( + + + + ) : ( + + )} + + ) +} + +export const InactiveButtonShowsDialog = () => { + const [isOpen, setIsOpen] = React.useState(false) + const onDialogClose = React.useCallback(() => { + setIsOpen(false) + }, [setIsOpen]) + const onAnchorClick = React.useCallback(() => { + setIsOpen(true) + }, [setIsOpen]) + return ( + + + {isOpen && ( + + There is an outage so you cant read whats supposed to be here + + )} + + ) +} diff --git a/src/Button/Button.stories.tsx b/src/Button/Button.stories.tsx index 00456a231bb..793d5884195 100644 --- a/src/Button/Button.stories.tsx +++ b/src/Button/Button.stories.tsx @@ -26,6 +26,11 @@ Playground.argTypes = { type: 'boolean', }, }, + inactive: { + control: { + type: 'boolean', + }, + }, variant: { control: { type: 'radio', @@ -51,6 +56,7 @@ Playground.args = { block: false, size: 'medium', disabled: false, + inactive: false, variant: 'default', alignContent: 'center', trailingVisual: null, diff --git a/src/Button/ButtonBase.tsx b/src/Button/ButtonBase.tsx index 8ee3b3ff6f3..1e05138e5c2 100644 --- a/src/Button/ButtonBase.tsx +++ b/src/Button/ButtonBase.tsx @@ -21,6 +21,7 @@ const ButtonBase = forwardRef( size = 'medium', alignContent = 'center', block = false, + inactive, ...rest } = props @@ -68,6 +69,7 @@ const ButtonBase = forwardRef( data-block={block ? 'block' : null} data-size={size === 'small' || size === 'large' ? size : undefined} data-no-visuals={!LeadingVisual && !TrailingVisual && !TrailingAction ? true : undefined} + data-inactive={inactive ? true : undefined} > {Icon ? ( diff --git a/src/Button/IconButton.docs.json b/src/Button/IconButton.docs.json index c49de7a2661..ad1529202a8 100644 --- a/src/Button/IconButton.docs.json +++ b/src/Button/IconButton.docs.json @@ -24,6 +24,11 @@ "defaultValue": "", "description": "Changes the size of the icon button component" }, + { + "name": "inactive", + "type": "boolean", + "description": "Whether the button looks visually disabled, but can still accept all the same interactions as an enabled button." + }, { "name": "icon", "type": "Component", diff --git a/src/Button/IconButton.stories.tsx b/src/Button/IconButton.stories.tsx index cc8b1f5b78a..a60e2fc9340 100644 --- a/src/Button/IconButton.stories.tsx +++ b/src/Button/IconButton.stories.tsx @@ -23,6 +23,11 @@ Playground.argTypes = { type: 'boolean', }, }, + inactive: { + control: { + type: 'boolean', + }, + }, variant: { control: { type: 'radio', @@ -34,6 +39,7 @@ Playground.argTypes = { Playground.args = { size: 'medium', disabled: false, + inactive: false, variant: 'default', 'aria-label': 'Icon button description', icon: XIcon, diff --git a/src/Button/__tests__/__snapshots__/Button.test.tsx.snap b/src/Button/__tests__/__snapshots__/Button.test.tsx.snap index 1d0805e2466..dfb478daddf 100644 --- a/src/Button/__tests__/__snapshots__/Button.test.tsx.snap +++ b/src/Button/__tests__/__snapshots__/Button.test.tsx.snap @@ -91,14 +91,13 @@ exports[`Button renders consistently 1`] = ` transition: none; } +.c0[data-inactive] { + cursor: auto; +} + .c0:disabled { cursor: not-allowed; box-shadow: none; - color: #8c959f; -} - -.c0:disabled [data-component=ButtonCounter] { - color: inherit; } .c0 [data-component=ButtonCounter] { @@ -156,6 +155,12 @@ exports[`Button renders consistently 1`] = ` width: 100%; } +.c0[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,#eaeef2); + border: 0; + color: var(--button-inactive-fgColor,#57606a); +} + .c0 [data-component="leadingVisual"] { grid-area: leadingVisual; } @@ -194,16 +199,26 @@ exports[`Button renders consistently 1`] = ` margin-right: 8px; } -.c0:hover:not([disabled]) { +.c0:hover:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: #f3f4f6; border-color: var(--button-default-borderColor-hover,rgba(31,35,40,0.15)); } -.c0:active:not([disabled]) { +.c0:active:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: hsla(220,14%,93%,1); border-color: var(--button-default-borderColor-active,rgba(31,35,40,0.15)); } +.c0:disabled, +.c0[aria-disabled] { + color: #8c959f; +} + +.c0:disabled [data-component=ButtonCounter], +.c0[aria-disabled] [data-component=ButtonCounter] { + color: inherit; +} + .c0[aria-expanded=true] { background-color: hsla(220,14%,93%,1); border-color: var(--button-default-borderColor-active,rgba(31,35,40,0.15)); @@ -326,14 +341,13 @@ exports[`Button respects block prop 1`] = ` transition: none; } +.c0[data-inactive] { + cursor: auto; +} + .c0:disabled { cursor: not-allowed; box-shadow: none; - color: primer.fg.disabled; -} - -.c0:disabled [data-component=ButtonCounter] { - color: inherit; } .c0 [data-component=ButtonCounter] { @@ -391,6 +405,12 @@ exports[`Button respects block prop 1`] = ` width: 100%; } +.c0[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,undefined); + border: 0; + color: var(--button-inactive-fgColor,undefined); +} + .c0 [data-component="leadingVisual"] { grid-area: leadingVisual; } @@ -429,16 +449,26 @@ exports[`Button respects block prop 1`] = ` margin-right: 8px; } -.c0:hover:not([disabled]) { +.c0:hover:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: btn.hoverBg; border-color: var(--button-default-borderColor-hover,undefined); } -.c0:active:not([disabled]) { +.c0:active:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: btn.activeBg; border-color: var(--button-default-borderColor-active,undefined); } +.c0:disabled, +.c0[aria-disabled] { + color: primer.fg.disabled; +} + +.c0:disabled [data-component=ButtonCounter], +.c0[aria-disabled] [data-component=ButtonCounter] { + color: inherit; +} + .c0[aria-expanded=true] { background-color: btn.activeBg; border-color: var(--button-default-borderColor-active,undefined); @@ -566,14 +596,13 @@ exports[`Button respects the alignContent prop 1`] = ` transition: none; } +.c0[data-inactive] { + cursor: auto; +} + .c0:disabled { cursor: not-allowed; box-shadow: none; - color: primer.fg.disabled; -} - -.c0:disabled [data-component=ButtonCounter] { - color: inherit; } .c0 [data-component=ButtonCounter] { @@ -631,6 +660,12 @@ exports[`Button respects the alignContent prop 1`] = ` width: 100%; } +.c0[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,undefined); + border: 0; + color: var(--button-inactive-fgColor,undefined); +} + .c0 [data-component="leadingVisual"] { grid-area: leadingVisual; } @@ -669,16 +704,26 @@ exports[`Button respects the alignContent prop 1`] = ` margin-right: 8px; } -.c0:hover:not([disabled]) { +.c0:hover:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: btn.hoverBg; border-color: var(--button-default-borderColor-hover,undefined); } -.c0:active:not([disabled]) { +.c0:active:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: btn.activeBg; border-color: var(--button-default-borderColor-active,undefined); } +.c0:disabled, +.c0[aria-disabled] { + color: primer.fg.disabled; +} + +.c0:disabled [data-component=ButtonCounter], +.c0[aria-disabled] [data-component=ButtonCounter] { + color: inherit; +} + .c0[aria-expanded=true] { background-color: btn.activeBg; border-color: var(--button-default-borderColor-active,undefined); @@ -805,14 +850,13 @@ exports[`Button respects the large size prop 1`] = ` transition: none; } +.c0[data-inactive] { + cursor: auto; +} + .c0:disabled { cursor: not-allowed; box-shadow: none; - color: primer.fg.disabled; -} - -.c0:disabled [data-component=ButtonCounter] { - color: inherit; } .c0 [data-component=ButtonCounter] { @@ -870,6 +914,12 @@ exports[`Button respects the large size prop 1`] = ` width: 100%; } +.c0[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,undefined); + border: 0; + color: var(--button-inactive-fgColor,undefined); +} + .c0 [data-component="leadingVisual"] { grid-area: leadingVisual; } @@ -908,16 +958,26 @@ exports[`Button respects the large size prop 1`] = ` margin-right: 8px; } -.c0:hover:not([disabled]) { +.c0:hover:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: btn.hoverBg; border-color: var(--button-default-borderColor-hover,undefined); } -.c0:active:not([disabled]) { +.c0:active:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: btn.activeBg; border-color: var(--button-default-borderColor-active,undefined); } +.c0:disabled, +.c0[aria-disabled] { + color: primer.fg.disabled; +} + +.c0:disabled [data-component=ButtonCounter], +.c0[aria-disabled] [data-component=ButtonCounter] { + color: inherit; +} + .c0[aria-expanded=true] { background-color: btn.activeBg; border-color: var(--button-default-borderColor-active,undefined); @@ -1045,14 +1105,13 @@ exports[`Button respects the small size prop 1`] = ` transition: none; } +.c0[data-inactive] { + cursor: auto; +} + .c0:disabled { cursor: not-allowed; box-shadow: none; - color: primer.fg.disabled; -} - -.c0:disabled [data-component=ButtonCounter] { - color: inherit; } .c0 [data-component=ButtonCounter] { @@ -1110,6 +1169,12 @@ exports[`Button respects the small size prop 1`] = ` width: 100%; } +.c0[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,undefined); + border: 0; + color: var(--button-inactive-fgColor,undefined); +} + .c0 [data-component="leadingVisual"] { grid-area: leadingVisual; } @@ -1148,16 +1213,26 @@ exports[`Button respects the small size prop 1`] = ` margin-right: 8px; } -.c0:hover:not([disabled]) { +.c0:hover:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: btn.hoverBg; border-color: var(--button-default-borderColor-hover,undefined); } -.c0:active:not([disabled]) { +.c0:active:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: btn.activeBg; border-color: var(--button-default-borderColor-active,undefined); } +.c0:disabled, +.c0[aria-disabled] { + color: primer.fg.disabled; +} + +.c0:disabled [data-component=ButtonCounter], +.c0[aria-disabled] [data-component=ButtonCounter] { + color: inherit; +} + .c0[aria-expanded=true] { background-color: btn.activeBg; border-color: var(--button-default-borderColor-active,undefined); @@ -1285,17 +1360,13 @@ exports[`Button styles danger button appropriately 1`] = ` transition: none; } +.c0[data-inactive] { + cursor: auto; +} + .c0:disabled { cursor: not-allowed; box-shadow: none; - color: btn.danger.disabledText; - background-color: btn.danger.disabledBg; - border-color: btn.danger.disabledBorder; -} - -.c0:disabled [data-component=ButtonCounter] { - color: btn.danger.disabledCounterFg; - background-color: btn.danger.disabledCounterBg; } .c0 [data-component=ButtonCounter] { @@ -1355,6 +1426,12 @@ exports[`Button styles danger button appropriately 1`] = ` width: 100%; } +.c0[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,undefined); + border: 0; + color: var(--button-inactive-fgColor,undefined); +} + .c0 [data-component="leadingVisual"] { grid-area: leadingVisual; } @@ -1393,25 +1470,38 @@ exports[`Button styles danger button appropriately 1`] = ` margin-right: 8px; } -.c0:hover:not([disabled]) { +.c0:hover:not([disabled]):not([aria-disabled]):not([data-inactive]) { color: btn.danger.hoverText; background-color: btn.danger.hoverBg; border-color: btn.danger.hoverBorder; box-shadow: undefined; } -.c0:hover:not([disabled]) [data-component=ButtonCounter] { +.c0:hover:not([disabled]):not([aria-disabled]):not([data-inactive]) [data-component=ButtonCounter] { background-color: btn.danger.hoverCounterBg; color: btn.danger.hoverCounterFg; } -.c0:active:not([disabled]) { +.c0:active:not([disabled]):not([aria-disabled]):not([data-inactive]) { color: btn.danger.selectedText; background-color: btn.danger.selectedBg; box-shadow: undefined; border-color: btn.danger.selectedBorder; } +.c0:disabled, +.c0[aria-disabled] { + color: btn.danger.disabledText; + background-color: btn.danger.disabledBg; + border-color: btn.danger.disabledBorder; +} + +.c0:disabled [data-component=ButtonCounter], +.c0[aria-disabled] [data-component=ButtonCounter] { + color: btn.danger.disabledCounterFg; + background-color: btn.danger.disabledCounterBg; +} + .c0[aria-expanded=true] { color: btn.danger.selectedText; background-color: btn.danger.selectedBg; @@ -1534,16 +1624,13 @@ exports[`Button styles invisible button appropriately 1`] = ` transition: none; } +.c0[data-inactive] { + cursor: auto; +} + .c0:disabled { cursor: not-allowed; box-shadow: none; - color: primer.fg.disabled; -} - -.c0:disabled [data-component=ButtonCounter], -.c0:disabled [data-component="leadingVisual"], -.c0:disabled [data-component="trailingAction"] { - color: inherit; } .c0 [data-component=ButtonCounter] { @@ -1601,6 +1688,12 @@ exports[`Button styles invisible button appropriately 1`] = ` width: 100%; } +.c0[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,undefined); + border: 0; + color: var(--button-inactive-fgColor,undefined); +} + .c0 [data-component="leadingVisual"] { grid-area: leadingVisual; color: fg.muted; @@ -1649,6 +1742,20 @@ exports[`Button styles invisible button appropriately 1`] = ` background-color: actionListItem.default.activeBg; } +.c0:disabled, +.c0[aria-disabled] { + color: primer.fg.disabled; +} + +.c0:disabled [data-component=ButtonCounter], +.c0[aria-disabled] [data-component=ButtonCounter], +.c0:disabled [data-component="leadingVisual"], +.c0[aria-disabled] [data-component="leadingVisual"], +.c0:disabled [data-component="trailingAction"], +.c0[aria-disabled] [data-component="trailingAction"] { + color: inherit; +} + .c0[aria-expanded=true] { background-color: actionListItem.default.selectedBg; } @@ -1788,15 +1895,13 @@ exports[`Button styles primary button appropriately 1`] = ` transition: none; } +.c0[data-inactive] { + cursor: auto; +} + .c0:disabled { cursor: not-allowed; box-shadow: none; - color: btn.primary.disabledText; - background-color: btn.primary.disabledBg; -} - -.c0:disabled [data-component=ButtonCounter] { - color: inherit; } .c0 [data-component=ButtonCounter] { @@ -1856,6 +1961,12 @@ exports[`Button styles primary button appropriately 1`] = ` width: 100%; } +.c0[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,undefined); + border: 0; + color: var(--button-inactive-fgColor,undefined); +} + .c0 [data-component="leadingVisual"] { grid-area: leadingVisual; } @@ -1894,7 +2005,7 @@ exports[`Button styles primary button appropriately 1`] = ` margin-right: 8px; } -.c0:hover:not([disabled]) { +.c0:hover:not([disabled]):not([aria-disabled]):not([data-inactive]) { color: btn.primary.hoverText; background-color: btn.primary.hoverBg; } @@ -1907,11 +2018,22 @@ exports[`Button styles primary button appropriately 1`] = ` box-shadow: inset 0 0 0 3px; } -.c0:active:not([disabled]) { +.c0:active:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: btn.primary.selectedBg; box-shadow: undefined; } +.c0:disabled, +.c0[aria-disabled] { + color: btn.primary.disabledText; + background-color: btn.primary.disabledBg; +} + +.c0:disabled [data-component=ButtonCounter], +.c0[aria-disabled] [data-component=ButtonCounter] { + color: inherit; +} + .c0[aria-expanded=true] { background-color: btn.primary.selectedBg; box-shadow: undefined; diff --git a/src/Button/styles.ts b/src/Button/styles.ts index e373ffd7f07..263913dc6fd 100644 --- a/src/Button/styles.ts +++ b/src/Button/styles.ts @@ -7,15 +7,15 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme color: 'btn.text', backgroundColor: 'btn.bg', boxShadow: `${theme?.shadows.btn.shadow}, ${theme?.shadows.btn.insetShadow}`, - '&:hover:not([disabled])': { + '&:hover:not([disabled]):not([aria-disabled]):not([data-inactive])': { backgroundColor: 'btn.hoverBg', borderColor: `var(--button-default-borderColor-hover, ${theme?.colors.btn.hoverBorder})`, }, - '&:active:not([disabled])': { + '&:active:not([disabled]):not([aria-disabled]):not([data-inactive])': { backgroundColor: 'btn.activeBg', borderColor: `var(--button-default-borderColor-active, ${theme?.colors.btn.activeBorder})`, }, - '&:disabled': { + '&:disabled, &[aria-disabled]': { color: 'primer.fg.disabled', '[data-component=ButtonCounter]': { color: 'inherit', @@ -34,7 +34,7 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme backgroundColor: 'btn.primary.bg', borderColor: 'btn.primary.border', boxShadow: `${theme?.shadows.btn.primary.shadow}`, - '&:hover:not([disabled])': { + '&:hover:not([disabled]):not([aria-disabled]):not([data-inactive])': { color: 'btn.primary.hoverText', backgroundColor: 'btn.primary.hoverBg', }, @@ -44,11 +44,11 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme '&:focus-visible:not([disabled])': { boxShadow: 'inset 0 0 0 3px', }, - '&:active:not([disabled])': { + '&:active:not([disabled]):not([aria-disabled]):not([data-inactive])': { backgroundColor: 'btn.primary.selectedBg', boxShadow: `${theme?.shadows.btn.primary.selectedShadow}`, }, - '&:disabled': { + '&:disabled, &[aria-disabled]': { color: 'btn.primary.disabledText', backgroundColor: 'btn.primary.disabledBg', '[data-component=ButtonCounter]': { @@ -68,7 +68,7 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme color: 'btn.danger.text', backgroundColor: 'btn.bg', boxShadow: `${theme?.shadows.btn.shadow}`, - '&:hover:not([disabled])': { + '&:hover:not([disabled]):not([aria-disabled]):not([data-inactive])': { color: 'btn.danger.hoverText', backgroundColor: 'btn.danger.hoverBg', borderColor: 'btn.danger.hoverBorder', @@ -78,13 +78,13 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme color: 'btn.danger.hoverCounterFg', }, }, - '&:active:not([disabled])': { + '&:active:not([disabled]):not([aria-disabled]):not([data-inactive])': { color: 'btn.danger.selectedText', backgroundColor: 'btn.danger.selectedBg', boxShadow: `${theme?.shadows.btn.danger.selectedShadow}`, borderColor: 'btn.danger.selectedBorder', }, - '&:disabled': { + '&:disabled, &[aria-disabled]': { color: 'btn.danger.disabledText', backgroundColor: 'btn.danger.disabledBg', borderColor: 'btn.danger.disabledBorder', @@ -115,7 +115,7 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme '&:active:not([disabled])': { backgroundColor: 'actionListItem.default.activeBg', }, - '&:disabled': { + '&:disabled, &[aria-disabled]': { color: 'primer.fg.disabled', '[data-component=ButtonCounter], [data-component="leadingVisual"], [data-component="trailingAction"]': { color: 'inherit', @@ -152,7 +152,7 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme borderColor: `var(--button-default-borderColor-rest, ${theme?.colors.btn.border})`, backgroundColor: 'btn.bg', - '&:hover:not([disabled])': { + '&:hover:not([disabled]):not([aria-disabled]):not([data-inactive])': { color: 'btn.outline.hoverText', backgroundColor: 'btn.outline.hoverBg', borderColor: `${theme?.colors.btn.outline.hoverBorder}`, @@ -162,14 +162,14 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme color: 'btn.outline.hoverCounterFg', }, }, - '&:active:not([disabled])': { + '&:active:not([disabled]):not([aria-disabled]):not([data-inactive])': { color: 'btn.outline.selectedText', backgroundColor: 'btn.outline.selectedBg', boxShadow: `${theme?.shadows.btn.outline.selectedShadow}`, borderColor: `${theme?.colors.btn.outline.selectedBorder}`, }, - '&:disabled': { + '&:disabled, &[aria-disabled]': { color: 'btn.outline.disabledText', backgroundColor: 'btn.outline.disabledBg', borderColor: 'btn.border', @@ -190,6 +190,7 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme }, }, } + return style[variant] } @@ -226,6 +227,9 @@ export const getBaseStyles = (theme?: Theme) => ({ '&:active': { transition: 'none', }, + '&[data-inactive]': { + cursor: 'auto', + }, '&:disabled': { cursor: 'not-allowed', boxShadow: 'none', @@ -291,6 +295,11 @@ export const getButtonStyles = (theme?: Theme) => { '&[data-block="block"]': { width: '100%', }, + '&[data-inactive]:not([disabled]):not([aria-disabled])': { + backgroundColor: `var(--button-inactive-bgColor, ${theme?.colors.btn.inactive.bg})`, + border: 0, + color: `var(--button-inactive-fgColor, ${theme?.colors.btn.inactive.text})`, + }, '[data-component="leadingVisual"]': { gridArea: 'leadingVisual', }, diff --git a/src/Button/types.ts b/src/Button/types.ts index d2f3c252099..49ad7969eb9 100644 --- a/src/Button/types.ts +++ b/src/Button/types.ts @@ -35,6 +35,11 @@ export type ButtonBaseProps = { * Allow button width to fill its container. */ block?: boolean + /** + * Whether the button looks visually disabled, but can still accept all the same + * interactions as an enabled button. + */ + inactive?: boolean } & SxProp & React.ButtonHTMLAttributes diff --git a/src/Dialog/__snapshots__/Dialog.test.tsx.snap b/src/Dialog/__snapshots__/Dialog.test.tsx.snap index cb0b5cfda3a..daedbbc002f 100644 --- a/src/Dialog/__snapshots__/Dialog.test.tsx.snap +++ b/src/Dialog/__snapshots__/Dialog.test.tsx.snap @@ -164,16 +164,13 @@ exports[`Dialog renders consistently 1`] = ` transition: none; } +.c1[data-inactive] { + cursor: auto; +} + .c1:disabled { cursor: not-allowed; box-shadow: none; - color: #8c959f; -} - -.c1:disabled [data-component=ButtonCounter], -.c1:disabled [data-component="leadingVisual"], -.c1:disabled [data-component="trailingAction"] { - color: inherit; } .c1 [data-component=ButtonCounter] { @@ -231,6 +228,12 @@ exports[`Dialog renders consistently 1`] = ` width: 100%; } +.c1[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,#eaeef2); + border: 0; + color: var(--button-inactive-fgColor,#57606a); +} + .c1 [data-component="leadingVisual"] { grid-area: leadingVisual; color: #656d76; @@ -279,6 +282,20 @@ exports[`Dialog renders consistently 1`] = ` background-color: rgba(208,215,222,0.48); } +.c1:disabled, +.c1[aria-disabled] { + color: #8c959f; +} + +.c1:disabled [data-component=ButtonCounter], +.c1[aria-disabled] [data-component=ButtonCounter], +.c1:disabled [data-component="leadingVisual"], +.c1[aria-disabled] [data-component="leadingVisual"], +.c1:disabled [data-component="trailingAction"], +.c1[aria-disabled] [data-component="trailingAction"] { + color: inherit; +} + .c1[aria-expanded=true] { background-color: rgba(208,215,222,0.24); } diff --git a/src/SelectPanel/__snapshots__/SelectPanel.test.tsx.snap b/src/SelectPanel/__snapshots__/SelectPanel.test.tsx.snap index 7d696d62efc..e3ec074e3c9 100644 --- a/src/SelectPanel/__snapshots__/SelectPanel.test.tsx.snap +++ b/src/SelectPanel/__snapshots__/SelectPanel.test.tsx.snap @@ -105,14 +105,13 @@ exports[`SelectPanel renders consistently 1`] = ` transition: none; } +.c1[data-inactive] { + cursor: auto; +} + .c1:disabled { cursor: not-allowed; box-shadow: none; - color: #8c959f; -} - -.c1:disabled [data-component=ButtonCounter] { - color: inherit; } .c1 [data-component=ButtonCounter] { @@ -170,6 +169,12 @@ exports[`SelectPanel renders consistently 1`] = ` width: 100%; } +.c1[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,#eaeef2); + border: 0; + color: var(--button-inactive-fgColor,#57606a); +} + .c1 [data-component="leadingVisual"] { grid-area: leadingVisual; } @@ -208,16 +213,26 @@ exports[`SelectPanel renders consistently 1`] = ` margin-right: 8px; } -.c1:hover:not([disabled]) { +.c1:hover:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: #f3f4f6; border-color: var(--button-default-borderColor-hover,rgba(31,35,40,0.15)); } -.c1:active:not([disabled]) { +.c1:active:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: hsla(220,14%,93%,1); border-color: var(--button-default-borderColor-active,rgba(31,35,40,0.15)); } +.c1:disabled, +.c1[aria-disabled] { + color: #8c959f; +} + +.c1:disabled [data-component=ButtonCounter], +.c1[aria-disabled] [data-component=ButtonCounter] { + color: inherit; +} + .c1[aria-expanded=true] { background-color: hsla(220,14%,93%,1); border-color: var(--button-default-borderColor-active,rgba(31,35,40,0.15)); diff --git a/src/__tests__/__snapshots__/ActionMenu.test.tsx.snap b/src/__tests__/__snapshots__/ActionMenu.test.tsx.snap index 0893c7502ea..0cc2dc942f1 100644 --- a/src/__tests__/__snapshots__/ActionMenu.test.tsx.snap +++ b/src/__tests__/__snapshots__/ActionMenu.test.tsx.snap @@ -105,14 +105,13 @@ exports[`ActionMenu renders consistently 1`] = ` transition: none; } +.c1[data-inactive] { + cursor: auto; +} + .c1:disabled { cursor: not-allowed; box-shadow: none; - color: #8c959f; -} - -.c1:disabled [data-component=ButtonCounter] { - color: inherit; } .c1 [data-component=ButtonCounter] { @@ -170,6 +169,12 @@ exports[`ActionMenu renders consistently 1`] = ` width: 100%; } +.c1[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,#eaeef2); + border: 0; + color: var(--button-inactive-fgColor,#57606a); +} + .c1 [data-component="leadingVisual"] { grid-area: leadingVisual; } @@ -208,16 +213,26 @@ exports[`ActionMenu renders consistently 1`] = ` margin-right: 8px; } -.c1:hover:not([disabled]) { +.c1:hover:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: #f3f4f6; border-color: var(--button-default-borderColor-hover,rgba(31,35,40,0.15)); } -.c1:active:not([disabled]) { +.c1:active:not([disabled]):not([aria-disabled]):not([data-inactive]) { background-color: hsla(220,14%,93%,1); border-color: var(--button-default-borderColor-active,rgba(31,35,40,0.15)); } +.c1:disabled, +.c1[aria-disabled] { + color: #8c959f; +} + +.c1:disabled [data-component=ButtonCounter], +.c1[aria-disabled] [data-component=ButtonCounter] { + color: inherit; +} + .c1[aria-expanded=true] { background-color: hsla(220,14%,93%,1); border-color: var(--button-default-borderColor-active,rgba(31,35,40,0.15)); diff --git a/src/__tests__/__snapshots__/TextInput.test.tsx.snap b/src/__tests__/__snapshots__/TextInput.test.tsx.snap index 1a431197282..1486e65ed4e 100644 --- a/src/__tests__/__snapshots__/TextInput.test.tsx.snap +++ b/src/__tests__/__snapshots__/TextInput.test.tsx.snap @@ -1779,16 +1779,13 @@ exports[`TextInput renders trailingAction icon button 1`] = ` transition: none; } +.c5[data-inactive] { + cursor: auto; +} + .c5:disabled { cursor: not-allowed; box-shadow: none; - color: #8c959f; -} - -.c5:disabled [data-component=ButtonCounter], -.c5:disabled [data-component="leadingVisual"], -.c5:disabled [data-component="trailingAction"] { - color: inherit; } .c5 [data-component=ButtonCounter] { @@ -1846,6 +1843,12 @@ exports[`TextInput renders trailingAction icon button 1`] = ` width: 100%; } +.c5[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,#eaeef2); + border: 0; + color: var(--button-inactive-fgColor,#57606a); +} + .c5 [data-component="leadingVisual"] { grid-area: leadingVisual; color: #656d76; @@ -1894,6 +1897,20 @@ exports[`TextInput renders trailingAction icon button 1`] = ` background-color: rgba(208,215,222,0.48); } +.c5:disabled, +.c5[aria-disabled] { + color: #8c959f; +} + +.c5:disabled [data-component=ButtonCounter], +.c5[aria-disabled] [data-component=ButtonCounter], +.c5:disabled [data-component="leadingVisual"], +.c5[aria-disabled] [data-component="leadingVisual"], +.c5:disabled [data-component="trailingAction"], +.c5[aria-disabled] [data-component="trailingAction"] { + color: inherit; +} + .c5[aria-expanded=true] { background-color: rgba(208,215,222,0.24); } @@ -2432,16 +2449,13 @@ exports[`TextInput renders trailingAction text button 1`] = ` transition: none; } +.c4[data-inactive] { + cursor: auto; +} + .c4:disabled { cursor: not-allowed; box-shadow: none; - color: #8c959f; -} - -.c4:disabled [data-component=ButtonCounter], -.c4:disabled [data-component="leadingVisual"], -.c4:disabled [data-component="trailingAction"] { - color: inherit; } .c4 [data-component=ButtonCounter] { @@ -2499,6 +2513,12 @@ exports[`TextInput renders trailingAction text button 1`] = ` width: 100%; } +.c4[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,#eaeef2); + border: 0; + color: var(--button-inactive-fgColor,#57606a); +} + .c4 [data-component="leadingVisual"] { grid-area: leadingVisual; color: #656d76; @@ -2547,6 +2567,20 @@ exports[`TextInput renders trailingAction text button 1`] = ` background-color: rgba(208,215,222,0.48); } +.c4:disabled, +.c4[aria-disabled] { + color: #8c959f; +} + +.c4:disabled [data-component=ButtonCounter], +.c4[aria-disabled] [data-component=ButtonCounter], +.c4:disabled [data-component="leadingVisual"], +.c4[aria-disabled] [data-component="leadingVisual"], +.c4:disabled [data-component="trailingAction"], +.c4[aria-disabled] [data-component="trailingAction"] { + color: inherit; +} + .c4[aria-expanded=true] { background-color: rgba(208,215,222,0.24); } @@ -2837,16 +2871,13 @@ exports[`TextInput renders trailingAction text button with a tooltip 1`] = ` transition: none; } +.c5[data-inactive] { + cursor: auto; +} + .c5:disabled { cursor: not-allowed; box-shadow: none; - color: #8c959f; -} - -.c5:disabled [data-component=ButtonCounter], -.c5:disabled [data-component="leadingVisual"], -.c5:disabled [data-component="trailingAction"] { - color: inherit; } .c5 [data-component=ButtonCounter] { @@ -2904,6 +2935,12 @@ exports[`TextInput renders trailingAction text button with a tooltip 1`] = ` width: 100%; } +.c5[data-inactive]:not([disabled]):not([aria-disabled]) { + background-color: var(--button-inactive-bgColor,#eaeef2); + border: 0; + color: var(--button-inactive-fgColor,#57606a); +} + .c5 [data-component="leadingVisual"] { grid-area: leadingVisual; color: #656d76; @@ -2952,6 +2989,20 @@ exports[`TextInput renders trailingAction text button with a tooltip 1`] = ` background-color: rgba(208,215,222,0.48); } +.c5:disabled, +.c5[aria-disabled] { + color: #8c959f; +} + +.c5:disabled [data-component=ButtonCounter], +.c5[aria-disabled] [data-component=ButtonCounter], +.c5:disabled [data-component="leadingVisual"], +.c5[aria-disabled] [data-component="leadingVisual"], +.c5:disabled [data-component="trailingAction"], +.c5[aria-disabled] [data-component="trailingAction"] { + color: inherit; +} + .c5[aria-expanded=true] { background-color: rgba(208,215,222,0.24); } diff --git a/src/drafts/Button2/Button.docs.json b/src/drafts/Button2/Button.docs.json index b8a24e7a045..5b27f9006e2 100644 --- a/src/drafts/Button2/Button.docs.json +++ b/src/drafts/Button2/Button.docs.json @@ -32,6 +32,11 @@ "type": "React.ComponentType", "description": "An icon to display after the button text." }, + { + "name": "inactive", + "type": "boolean", + "description": "Whether the button looks visually disabled, but can still accept all the same interactions as an enabled button.\n This is intended to be used when a system error such as an outage prevents the button from performing its usual action.\n Inactive styles are slightly different from disabled styles because inactive buttons need to have an accessible color contrast ratio. This is because inactive buttons can have tooltips or perform an action such as opening a dialog explaining why it's inactive.\n If both `disabled` and `inactive` are true, `disabled` takes precedence." + }, { "name": "as", "type": "React.ElementType", diff --git a/src/drafts/Button2/Button.features.stories.tsx b/src/drafts/Button2/Button.features.stories.tsx index ab0fce76123..0acd2902e1e 100644 --- a/src/drafts/Button2/Button.features.stories.tsx +++ b/src/drafts/Button2/Button.features.stories.tsx @@ -34,6 +34,8 @@ export const Block = () => export const Disabled = () => +export const Inactive = () => + export const Small = () => export const Medium = () => diff --git a/src/drafts/Button2/Button.stories.tsx b/src/drafts/Button2/Button.stories.tsx index 32a841e66c4..8c3975485b8 100644 --- a/src/drafts/Button2/Button.stories.tsx +++ b/src/drafts/Button2/Button.stories.tsx @@ -6,55 +6,6 @@ import {OcticonArgType} from '../../utils/story-helpers' export default { title: 'Drafts/Components/Button', - argTypes: { - size: { - control: { - type: 'radio', - }, - options: ['small', 'medium', 'large'], - }, - disabled: { - control: { - type: 'boolean', - }, - }, - variant: { - control: { - type: 'radio', - }, - options: ['default', 'primary', 'danger', 'invisible', 'outline'], - }, - alignContent: { - control: { - type: 'radio', - }, - options: ['center', 'start'], - }, - block: { - control: { - type: 'boolean', - }, - }, - leadingIcon: OcticonArgType([EyeClosedIcon, EyeIcon, SearchIcon, XIcon, HeartIcon]), - trailingIcon: OcticonArgType([EyeClosedIcon, EyeIcon, SearchIcon, XIcon, HeartIcon]), - trailingAction: OcticonArgType([TriangleDownIcon]), - trailingVisualCount: { - control: { - type: 'number', - }, - }, - }, - args: { - block: false, - size: 'medium', - disabled: false, - variant: 'default', - alignContent: 'center', - trailingIcon: null, - leadingIcon: null, - trailingAction: null, - trailingVisualCount: undefined, - }, } as Meta export const Playground: StoryFn = args => { @@ -67,4 +18,61 @@ export const Playground: StoryFn = args => { ) } +Playground.argTypes = { + size: { + control: { + type: 'radio', + }, + options: ['small', 'medium', 'large'], + }, + disabled: { + control: { + type: 'boolean', + }, + }, + inactive: { + control: { + type: 'boolean', + }, + }, + variant: { + control: { + type: 'radio', + }, + options: ['default', 'primary', 'danger', 'invisible', 'outline'], + }, + alignContent: { + control: { + type: 'radio', + }, + options: ['center', 'start'], + }, + block: { + control: { + type: 'boolean', + }, + }, + leadingIcon: OcticonArgType([EyeClosedIcon, EyeIcon, SearchIcon, XIcon, HeartIcon]), + trailingIcon: OcticonArgType([EyeClosedIcon, EyeIcon, SearchIcon, XIcon, HeartIcon]), + trailingAction: OcticonArgType([TriangleDownIcon]), + trailingVisualCount: { + control: { + type: 'number', + }, + }, +} + +Playground.args = { + block: false, + size: 'medium', + disabled: false, + inactive: false, + variant: 'default', + alignContent: 'center', + trailingIcon: null, + leadingIcon: null, + trailingAction: null, + trailingVisualCount: undefined, +} + export const Default = () => diff --git a/src/drafts/Button2/Button.tsx b/src/drafts/Button2/Button.tsx index d3cce915480..9f02953aae3 100644 --- a/src/drafts/Button2/Button.tsx +++ b/src/drafts/Button2/Button.tsx @@ -8,11 +8,11 @@ import {BetterSystemStyleObject} from '../../sx' const ButtonComponent = forwardRef(({children, sx: sxProp = defaultSxProp, ...props}, forwardedRef): JSX.Element => { let sxStyles = sxProp - // grap the button props that have associated data attributes in the styles - const {block, size, leadingIcon, trailingIcon, trailingAction} = props + // grab the button props that have associated data attributes in the styles + const {block, size, leadingIcon, trailingIcon, trailingAction, inactive} = props if (sxProp !== null && Object.keys(sxProp).length > 0) { - sxStyles = generateCustomSxProp({block, size, leadingIcon, trailingIcon, trailingAction}, sxProp) + sxStyles = generateCustomSxProp({block, size, leadingIcon, trailingIcon, trailingAction, inactive}, sxProp) } return ( @@ -63,16 +63,17 @@ sx={{ // We need to make sure we append the customCSSSelector to the original class selector. i.e & - > &[data-attribute="Icon"][data-size="small"] */ export function generateCustomSxProp( - props: Partial>, + props: Partial>, providedSx: BetterSystemStyleObject, ) { // Possible data attributes: data-size, data-block, data-no-visuals const size = props.size && props.size !== 'medium' ? `[data-size="${props.size}"]` : '' // medium is a default size therefore it doesn't have a data attribute that used for styling const block = props.block ? `[data-block="block"]` : '' const noVisuals = props.leadingIcon || props.trailingIcon || props.trailingAction ? '' : '[data-no-visuals="true"]' + const inactive = props.inactive ? '[data-inactive="true"]' : '' // this is custom selector. We need to make sure we add the data attributes to the base css class (& -> &[data-attributename="value"]]) - const cssSelector = `&${size}${block}${noVisuals}` // &[data-size="small"][data-block="block"][data-no-visuals="true"] + const cssSelector = `&${size}${block}${noVisuals}${inactive}` // &[data-size="small"][data-block="block"][data-no-visuals="true"] const customSxProp: { [key: string]: BetterSystemStyleObject diff --git a/src/drafts/Button2/ButtonBase.tsx b/src/drafts/Button2/ButtonBase.tsx index 44125f7dbd0..84c30c19863 100644 --- a/src/drafts/Button2/ButtonBase.tsx +++ b/src/drafts/Button2/ButtonBase.tsx @@ -19,6 +19,7 @@ const ButtonBase = forwardRef( size = 'medium', alignContent = 'center', block = false, + inactive, ...rest } = props @@ -27,8 +28,8 @@ const ButtonBase = forwardRef( const {theme} = useTheme() const baseStyles = useMemo(() => { - return merge.all([getButtonStyles(theme), getVariantStyles(variant, theme)]) - }, [theme, variant]) + return merge.all([getButtonStyles(theme), getVariantStyles(variant, inactive, theme)]) + }, [inactive, theme, variant]) const sxStyles = useMemo(() => { return merge(baseStyles, sxProp) }, [baseStyles, sxProp]) @@ -66,6 +67,7 @@ const ButtonBase = forwardRef( data-block={block ? 'block' : null} data-size={size === 'small' || size === 'large' ? size : undefined} data-no-visuals={!LeadingIcon && !TrailingIcon && !TrailingAction ? true : undefined} + data-inactive={inactive ? true : undefined} > {Icon ? ( diff --git a/src/drafts/Button2/styles.ts b/src/drafts/Button2/styles.ts index 474a9e81991..86c9574c74b 100644 --- a/src/drafts/Button2/styles.ts +++ b/src/drafts/Button2/styles.ts @@ -1,21 +1,21 @@ import {VariantType, AlignContent} from './types' import {Theme} from '../../ThemeProvider' -export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme) => { +export const getVariantStyles = (variant: VariantType = 'default', inactive?: boolean, theme?: Theme) => { const style = { default: { color: 'btn.text', backgroundColor: 'btn.bg', boxShadow: `${theme?.shadows.btn.shadow}, ${theme?.shadows.btn.insetShadow}`, - '&:hover:not([disabled])': { + '&:hover:not([disabled]):not([aria-disabled]):not([data-inactive])': { backgroundColor: 'btn.hoverBg', borderColor: 'btn.hoverBorder', }, - '&:active:not([disabled])': { + '&:active:not([disabled]):not([aria-disabled]):not([data-inactive])': { backgroundColor: 'btn.activeBg', borderColor: 'btn.activeBorder', }, - '&:disabled': { + '&:disabled, &[aria-disabled]': { color: 'primer.fg.disabled', '[data-component=ButtonCounter]': { color: 'inherit', @@ -31,7 +31,7 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme backgroundColor: 'btn.primary.bg', borderColor: 'btn.primary.border', boxShadow: `${theme?.shadows.btn.primary.shadow}`, - '&:hover:not([disabled])': { + '&:hover:not([disabled]):not([aria-disabled]):not([data-inactive])': { color: 'btn.primary.hoverText', backgroundColor: 'btn.primary.hoverBg', }, @@ -41,11 +41,11 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme '&:focus-visible:not([disabled])': { boxShadow: 'inset 0 0 0 3px', }, - '&:active:not([disabled])': { + '&:active:not([disabled]):not([aria-disabled]):not([data-inactive])': { backgroundColor: 'btn.primary.selectedBg', boxShadow: `${theme?.shadows.btn.primary.selectedShadow}`, }, - '&:disabled': { + '&:disabled, &[aria-disabled]': { color: 'btn.primary.disabledText', backgroundColor: 'btn.primary.disabledBg', '[data-component=ButtonCounter]': { @@ -65,7 +65,7 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme color: 'btn.danger.text', backgroundColor: 'btn.bg', boxShadow: `${theme?.shadows.btn.shadow}`, - '&:hover:not([disabled])': { + '&:hover:not([disabled]):not([aria-disabled]):not([data-inactive])': { color: 'btn.danger.hoverText', backgroundColor: 'btn.danger.hoverBg', borderColor: 'btn.danger.hoverBorder', @@ -75,13 +75,13 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme color: 'btn.danger.hoverText', }, }, - '&:active:not([disabled])': { + '&:active:not([disabled]):not([aria-disabled]):not([data-inactive])': { color: 'btn.danger.selectedText', backgroundColor: 'btn.danger.selectedBg', boxShadow: `${theme?.shadows.btn.danger.selectedShadow}`, borderColor: 'btn.danger.selectedBorder', }, - '&:disabled': { + '&:disabled, &[aria-disabled]': { color: 'btn.danger.disabledText', backgroundColor: 'btn.danger.disabledBg', borderColor: 'btn.danger.disabledBorder', @@ -106,13 +106,13 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme backgroundColor: 'transparent', borderColor: 'transparent', boxShadow: 'none', - '&:hover:not([disabled])': { + '&:hover:not([disabled]):not([data-inactive])': { backgroundColor: 'btn.hoverBg', }, - '&:active:not([disabled])': { + '&:active:not([disabled]):not([aria-disabled]):not([data-inactive])': { backgroundColor: 'btn.selectedBg', }, - '&:disabled': { + '&:disabled, &[aria-disabled]': { color: 'primer.fg.disabled', '[data-component=ButtonCounter]': { color: 'inherit', @@ -143,7 +143,7 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme borderColor: 'btn.border', backgroundColor: 'btn.bg', - '&:hover:not([disabled])': { + '&:hover:not([disabled]):not([aria-disabled]):not([data-inactive])': { color: 'btn.outline.hoverText', backgroundColor: 'btn.outline.hoverBg', borderColor: 'btn.outline.hoverBorder', @@ -153,14 +153,14 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme color: 'inherit', }, }, - '&:active:not([disabled])': { + '&:active:not([disabled]):not([aria-disabled]):not([data-inactive])': { color: 'btn.outline.selectedText', backgroundColor: 'btn.outline.selectedBg', boxShadow: `${theme?.shadows.btn.outline.selectedShadow}`, borderColor: 'btn.outline.selectedBorder', }, - '&:disabled': { + '&:disabled, &[aria-disabled]': { color: 'btn.outline.disabledText', backgroundColor: 'btn.outline.disabledBg', borderColor: 'btn.border', @@ -181,6 +181,7 @@ export const getVariantStyles = (variant: VariantType = 'default', theme?: Theme }, }, } + return style[variant] } @@ -217,6 +218,9 @@ export const getBaseStyles = (theme?: Theme) => ({ '&:active': { transition: 'none', }, + '&[data-inactive]': { + cursor: 'auto', + }, '&:disabled': { cursor: 'not-allowed', boxShadow: 'none', @@ -282,6 +286,11 @@ export const getButtonStyles = (theme?: Theme) => { '&[data-block="block"]': { width: '100%', }, + '&[data-inactive]:not([disabled]):not([aria-disabled])': { + backgroundColor: `var(--button-inactive-bgColor, ${theme?.colors.btn.inactive.bg})`, + border: 0, + color: `var(--button-inactive-fgColor, ${theme?.colors.btn.inactive.text})`, + }, '[data-component="leadingVisual"]': { gridArea: 'leadingVisual', }, diff --git a/src/drafts/Button2/types.ts b/src/drafts/Button2/types.ts index 7194fd0d354..b1dcdd065ff 100644 --- a/src/drafts/Button2/types.ts +++ b/src/drafts/Button2/types.ts @@ -31,6 +31,11 @@ export type ButtonBaseProps = { * Items that are disabled can not be clicked, selected, or navigated through. */ disabled?: boolean + /** + * Whether the button looks visually disabled, but can still accept all the same + * interactions as an enabled button. + */ + inactive?: boolean /** * Allow button width to fill its container. */