diff --git a/docs/src/stories/explorations/Button.stories.jsx b/docs/src/stories/explorations/Button.stories.jsx new file mode 100644 index 0000000000..87e92c429c --- /dev/null +++ b/docs/src/stories/explorations/Button.stories.jsx @@ -0,0 +1,207 @@ +import React from 'react' +import clsx from 'clsx' + +export default { + title: 'Explorations/Button', + parameters: { + layout: 'padded' + }, + + excludeStories: ['ButtonTemplate'], + argTypes: { + variant: { + options: [0, 1, 2, 3], // iterator + mapping: ['Button--secondary', 'Button--primary', 'Button--invisible', 'Button--danger'], // values + control: { + type: 'inline-radio', + labels: ['secondary', 'primary', 'invisible', 'danger'] + }, + table: { + category: 'CSS' + }, + description: 'Controls button color', + defaultValue: 'Button--secondary' + }, + size: { + options: [0, 1, 2], // iterator + mapping: [null, 'Button--small', 'Button--large'], // values + control: { + type: 'inline-radio', + labels: ['default [32px]', 'small [28px]', 'large [40px]'] + }, + table: { + category: 'CSS' + }, + description: 'Controls button height', + defaultValue: 0 + }, + alignContent: { + options: [0, 1], // iterator + mapping: [null, 'Button-content--alignStart'], // values + control: { + type: 'inline-radio', + labels: ['center [default]', 'start'] + }, + table: { + category: 'CSS' + }, + description: + 'Align button label + visuals to the center (default for CTA buttons) or start for select/dropdown button scenarios', + defaultValue: 0 + }, + label: { + defaultValue: 'Button', + type: 'string', + name: 'label', + description: 'Visible button label', + table: { + category: 'Slot' + } + }, + ariaLabel: { + defaultValue: '', + type: 'string', + name: 'ariaLabel', + description: 'Hidden button label (in addition to visible label). Not required in all cases.', + table: { + category: 'Slot' + } + }, + disabled: { + defaultValue: false, + control: {type: 'boolean'}, + table: { + category: 'State' + } + }, + fullWidth: { + defaultValue: false, + control: {type: 'boolean'}, + description: 'Allow button to stretch and fill container', + table: { + category: 'CSS' + } + }, + leadingVisual: { + name: 'leadingVisual', + control: {type: 'boolean'}, + description: 'Slot for SVG icon or emoji (boolean only for testing purposes)', + defaultValue: false, + table: { + category: 'Slot' + } + }, + trailingVisual: { + name: 'trailingVisual', + control: {type: 'boolean'}, + description: 'Slot for SVG icon or emoji (boolean only for testing purposes)', + table: { + category: 'Slot' + }, + defaultValue: false + }, + trailingAction: { + defaultValue: false, + control: {type: 'boolean'}, + description: + 'Slot for SVG icon that indicates an action. Primarily used by other Primer components, like a DropdownMenu or overlay trigger (boolean only for testing purposes)', + table: { + category: 'Slot' + } + }, + pressed: { + defaultValue: false, + control: {type: 'boolean'}, + table: { + category: 'State' + } + }, + focusElement: { + control: {type: 'boolean'}, + description: 'set focus on one element', + table: { + category: 'State' + } + }, + active: { + control: {type: 'boolean'}, + description: 'set button to active state', + table: { + category: 'State' + } + } + } +} + +const focusMethod = function getFocus() { + // find the focusable element + var button = document.getElementsByTagName('button')[0] + // set focus on element + button.focus() +} + +const star = ( + + + +) + +const caret = ( + + + +) + +export const ButtonTemplate = ({ + label, + variant, + disabled, + size, + fullWidth, + leadingVisual, + trailingVisual, + trailingAction, + pressed, + focusElement, + active, + className, + ariaLabel, + alignContent +}) => ( + <> + + {focusElement && focusMethod()} + +) + +export const Playground = ButtonTemplate.bind({}) +Playground.args = { + focusElement: false, + active: false, + variant: 'Button--secondary', + leadingVisual: false, + trailingAction: false, + trailingVisual: false +} diff --git a/docs/src/stories/explorations/ButtonGroup.stories.jsx b/docs/src/stories/explorations/ButtonGroup.stories.jsx new file mode 100644 index 0000000000..b91ae511cd --- /dev/null +++ b/docs/src/stories/explorations/ButtonGroup.stories.jsx @@ -0,0 +1,24 @@ +import React from 'react' +import clsx from 'clsx' +import {ButtonTemplate} from './Button.stories' +import {IconButtonTemplate} from './IconButton.stories' + +export default { + title: 'Explorations/ButtonGroup', + excludeStories: ['ButtonGroupTemplate'], + layout: 'padded', + argTypes: {} +} + +// build every component case here in the template (private api) +export const ButtonGroupTemplate = ({}) => ( +
+ + + + +
+) + +export const Playground = ButtonGroupTemplate.bind({}) +Playground.args = {} diff --git a/docs/src/stories/explorations/ExampleSheet.stories.jsx b/docs/src/stories/explorations/ExampleSheet.stories.jsx new file mode 100644 index 0000000000..2f539aa70f --- /dev/null +++ b/docs/src/stories/explorations/ExampleSheet.stories.jsx @@ -0,0 +1,986 @@ +import React from 'react' +import clsx from 'clsx' +import {ButtonTemplate} from './Button.stories' +import {IconButtonTemplate} from './IconButton.stories' +import {LinkTemplate} from './Link.stories' +import {LinkStyledAsButtonTemplate} from './LinkStyledAsButton.stories' +import {ButtonGroupTemplate} from './ButtonGroup.stories' + +export default { + title: 'Explorations', + layout: 'padded', + argTypes: {} +} + +const gridStyle = { + display: 'grid', + gridAutoFlow: 'column dense', + gap: '16px', + justifyItems: 'start' +} + +const gridStyleStretch = { + display: 'grid', + gridAutoFlow: 'column', + gap: '16px', + justifyItems: 'start', + width: '100%' +} + +export const ExampleSheet = ({}) => ( +
+

Standard

+
+ + + + +
+
+ + + + +
+
+ + + + +
+

Leading visual

+
+ + + + +
+
+ + + + +
+
+ + + + +
+

Trailing visual

+
+ + + + +
+
+ + + + +
+
+ + + + +
+

Leading + Trailing visual

+
+ + + + +
+
+ + + + +
+
+ + + + +
+ +

Trailing action

+
+ + + + +
+
+ + + + +
+
+ + + + +
+ +

Fullwidth (all visual scenarios, one button size)

+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+ +
+

Fullwidth (all visual scenarios, one button size) visualPosition fixed

+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+ +

Leading visual + action

+
+ + + + +
+
+ + + + +
+
+ + + + +
+

Trailing visual + action

+
+ + + + +
+
+ + + + +
+
+ + + + +
+

Leading + Trailing visual + action

+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+

IconButton

+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+

LinkStyledAsButton

+
+ + + + +
+
+ + + + +
+
+ + + + +
+

Leading visual

+
+ + + + +
+
+ + + + +
+
+ + + + +
+

Trailing visual

+
+ + + + +
+
+ + + + +
+
+ + + + +
+

Leading + Trailing visual

+
+ + + + +
+
+ + + + +
+
+ + + + +
+ +

Trailing action

+
+ + + + +
+
+ + + + +
+
+ + + + +
+ +

Leading visual + action

+
+ + + + +
+
+ + + + +
+
+ + + + +
+

Trailing visual + action

+
+ + + + +
+
+ + + + +
+
+ + + + +
+

Leading + Trailing visual + action

+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+

Link

+
+ + +
+
+ + +
+
+) + +// export const Playground = ExampleSheetTemplate.bind({}) +// Playground.args = {} diff --git a/docs/src/stories/explorations/IconButton.stories.jsx b/docs/src/stories/explorations/IconButton.stories.jsx new file mode 100644 index 0000000000..83acd9c942 --- /dev/null +++ b/docs/src/stories/explorations/IconButton.stories.jsx @@ -0,0 +1,130 @@ +import React from 'react' +import clsx from 'clsx' + +export default { + title: 'Explorations/IconButton', + parameters: { + // design: { + // type: 'figma', + // url: 'https://www.figma.com/file/GCvY3Qv8czRgZgvl1dG6lp/Primer-Web?node-id=4371%3A7128' + // }, + layout: 'padded' + }, + + excludeStories: ['IconButtonTemplate'], + argTypes: { + variant: { + options: [0, 1, 2, 3], // iterator + mapping: ['Button--secondary', 'Button--primary', 'Button--invisible', 'Button--danger'], // values + control: { + type: 'inline-radio', + labels: ['default', 'primary', 'outline', 'danger'] + }, + table: { + category: 'CSS' + }, + description: 'Controls button color', + defaultValue: 'Button--secondary' + }, + size: { + options: [0, 1, 2], // iterator + mapping: [null, 'Button--small', 'Button--large'], // values + control: { + type: 'inline-radio', + labels: ['default [32px]', 'small [28px]', 'large [40px]'] + }, + table: { + category: 'CSS' + }, + description: 'Controls button height and width', + defaultValue: 0 + }, + ariaLabel: { + defaultValue: '', + type: 'string', + name: 'ariaLabel', + description: 'Hidden button label (required)', + table: { + category: 'Slot' + } + }, + disabled: { + defaultValue: false, + control: {type: 'boolean'}, + table: { + category: 'State' + } + }, + visual: { + name: 'visual', + control: {type: 'boolean'}, + description: 'Slot for SVG icon (boolean only for testing purposes)', + defaultValue: true, + table: { + category: 'Slot' + } + }, + pressed: { + defaultValue: false, + control: {type: 'boolean'}, + table: { + category: 'State' + } + }, + focusElement: { + control: {type: 'boolean'}, + description: 'set focus on one element', + table: { + category: 'State' + } + } + } +} + +const focusMethod = function getFocus() { + // find the focusable element + var button = document.getElementsByTagName('button')[0] + // set focus on element + button.focus() +} + +const star = ( + + + +) + +const caret = ( + + + +) + +export const IconButtonTemplate = ({variant, disabled, size, visual, pressed, focusElement, className, ariaLabel}) => ( + <> + + {focusElement && focusMethod()} + +) + +export const Playground = IconButtonTemplate.bind({}) +Playground.args = { + focusElement: false, + ariaLabel: 'Button description' +} diff --git a/docs/src/stories/explorations/Link.stories.jsx b/docs/src/stories/explorations/Link.stories.jsx new file mode 100644 index 0000000000..7379937b1f --- /dev/null +++ b/docs/src/stories/explorations/Link.stories.jsx @@ -0,0 +1,130 @@ +import React from 'react' +import clsx from 'clsx' + +export default { + title: 'Explorations/Link', + parameters: { + // design: { + // type: 'figma', + // url: 'https://www.figma.com/file/GCvY3Qv8czRgZgvl1dG6lp/Primer-Web?node-id=4371%3A7128' + // }, + layout: 'padded' + }, + + excludeStories: ['LinkTemplate'], + argTypes: { + variant: { + options: [0, 1], // iterator + mapping: [null, 'Link--muted'], // values + control: { + type: 'inline-radio', + labels: ['default', 'muted'] + }, + table: { + category: 'CSS' + }, + description: 'Controls link color', + defaultValue: 0 + }, + label: { + defaultValue: 'Link', + type: 'string', + name: 'label', + description: 'Link label', + table: { + category: 'Slot' + } + }, + target: { + defaultValue: '', + type: 'string', + name: 'target', + description: '_blank for new tab', + table: { + category: 'Slot' + } + }, + href: { + defaultValue: '', + type: 'string', + name: 'href', + description: '', + table: { + category: 'Slot' + } + }, + showTrailingAction: { + defaultValue: false, + control: {type: 'boolean'}, + description: '', + table: { + category: 'CSS' + } + }, + focusElement: { + control: {type: 'boolean'}, + description: 'set focus on one element', + table: { + category: 'State' + } + }, + focusAllElements: { + control: {type: 'boolean'}, + description: 'set focus on all elements for viewing in all themes', + table: { + category: 'State' + } + } + } +} + +const focusMethod = function getFocus() { + // find the focusable element + var button = document.getElementsByTagName('button')[0] + // set focus on element + button.focus() +} + +const arrow = ( + + + +) + +export const LinkTemplate = ({ + label, + variant, + focusElement, + focusAllElements, + className, + href, + showTrailingAction, + target +}) => ( + <> + + {showTrailingAction ? ( + + {label} + {arrow} + + ) : ( + label + )} + + {focusElement && focusMethod()} + +) + +export const Playground = LinkTemplate.bind({}) +Playground.args = { + focusElement: false, + focusAllElements: false +} diff --git a/docs/src/stories/explorations/LinkStyledAsButton.stories.jsx b/docs/src/stories/explorations/LinkStyledAsButton.stories.jsx new file mode 100644 index 0000000000..0efd391a4e --- /dev/null +++ b/docs/src/stories/explorations/LinkStyledAsButton.stories.jsx @@ -0,0 +1,228 @@ +import React from 'react' +import clsx from 'clsx' + +export default { + title: 'Explorations/LinkStyledAsButton', + parameters: { + // design: { + // type: 'figma', + // url: 'https://www.figma.com/file/GCvY3Qv8czRgZgvl1dG6lp/Primer-Web?node-id=4371%3A7128' + // }, + layout: 'padded' + }, + + excludeStories: ['LinkStyledAsButtonTemplate'], + argTypes: { + variant: { + options: [0, 1, 2, 3], // iterator + mapping: [ + 'LinkStyledAsButton--secondary', + 'LinkStyledAsButton--primary', + 'LinkStyledAsButton--invisible', + 'LinkStyledAsButton--danger' + ], // values + control: { + type: 'inline-radio', + labels: ['secondary', 'primary', 'invisible', 'danger'] + }, + table: { + category: 'CSS' + }, + description: 'Controls button color', + defaultValue: 'LinkStyledAsButton--secondary' + }, + size: { + options: [0, 1, 2], // iterator + mapping: [null, 'LinkStyledAsButton--small', 'LinkStyledAsButton--large'], // values + control: { + type: 'inline-radio', + labels: ['default [32px]', 'small [28px]', 'large [40px]'] + }, + table: { + category: 'CSS' + }, + description: 'Controls button height', + defaultValue: 0 + }, + alignContent: { + options: [0, 1], // iterator + mapping: [null, 'Button-content--alignStart'], // values + control: { + type: 'inline-radio', + labels: ['center [default]', 'start'] + }, + table: { + category: 'CSS' + }, + description: + 'Align button label + visuals to the center (default for CTA buttons) or start for select/dropdown button scenarios', + defaultValue: 0 + }, + label: { + defaultValue: 'Link', + type: 'string', + name: 'label', + description: 'Visible button label', + table: { + category: 'Slot' + } + }, + target: { + defaultValue: '', + type: 'string', + name: 'target', + description: '_blank for new tab', + table: { + category: 'Slot' + } + }, + href: { + defaultValue: '', + type: 'string', + name: 'href', + description: '', + table: { + category: 'Slot' + } + }, + disabled: { + defaultValue: false, + control: {type: 'boolean'}, + table: { + category: 'State' + } + }, + fullWidth: { + defaultValue: false, + control: {type: 'boolean'}, + description: 'Allow button to stretch and fill container', + table: { + category: 'CSS' + } + }, + leadingVisual: { + name: 'leadingVisual', + control: {type: 'boolean'}, + description: 'Slot for SVG icon or emoji (boolean only for testing purposes)', + defaultValue: false, + table: { + category: 'Slot' + } + }, + trailingVisual: { + name: 'trailingVisual', + control: {type: 'boolean'}, + description: 'Slot for SVG icon or emoji (boolean only for testing purposes)', + table: { + category: 'Slot' + }, + defaultValue: false + }, + showTrailingAction: { + defaultValue: false, + control: {type: 'boolean'}, + description: + 'Slot for SVG icon that indicates an action. Primarily used by other Primer components, like a DropdownMenu or overlay trigger (boolean only for testing purposes)', + table: { + category: 'Slot' + } + }, + pressed: { + defaultValue: false, + control: {type: 'boolean'}, + table: { + category: 'State' + } + }, + focusElement: { + control: {type: 'boolean'}, + description: 'set focus on one element', + table: { + category: 'State' + } + }, + active: { + control: {type: 'boolean'}, + description: 'set button to active state', + table: { + category: 'State' + } + } + } +} + +const focusMethod = function getFocus() { + // find the focusable element + var button = document.getElementsByTagName('button')[0] + // set focus on element + button.focus() +} + +const star = ( + + + +) + +const arrow = ( + + + +) + +export const LinkStyledAsButtonTemplate = ({ + label, + variant, + size, + fullWidth, + leadingVisual, + trailingVisual, + showTrailingAction, + focusElement, + active, + alignContent, + className, + href, + target +}) => ( + <> + + + {leadingVisual && {star}} + {label} + {trailingVisual && {star}} + + {showTrailingAction && ( + {arrow} + )} + + {focusElement && focusMethod()} + +) + +export const Playground = LinkStyledAsButtonTemplate.bind({}) +Playground.args = { + focusElement: false, + active: false, + variant: 'LinkStyledAsButton--secondary', + leadingVisual: false, + showTrailingAction: false, + trailingVisual: false +} diff --git a/src/button-refactor/ButtonsLinksAPI.md b/src/button-refactor/ButtonsLinksAPI.md new file mode 100644 index 0000000000..ac24cf474d --- /dev/null +++ b/src/button-refactor/ButtonsLinksAPI.md @@ -0,0 +1,116 @@ +# Button and Link component APIs +Button and Link has been discussed in a number of issues and discussions over past few years. I went through the list below, took note of their purpose and started compiling a new API spec. This doc is is meant to be collaborative and accompany Storybook docs while we work through a final API recommendation. Once we have a solid plan, this can move into an issue and be delegated across frameworks. + +Existing issues +- https://github.com/github/primer/issues/141 + -- Accessory button component, should reconcile with reaction button (sister component to Button) +- https://github.com/github/primer/issues/220 + -- Tracking issue, mentions Button in the context of API consistency +- https://github.com/github/primer/issues/253 + -- Button audit, anatomy spec, design considerations, some button link discussion, icon buttons (this issue is very informative!) +- https://github.com/github/primer/issues/263 + -- More design discussion, particularly focused on outline button +- https://github.com/github/primer/issues/272 + -- Make icon only buttons square +- https://github.com/github/primer/issues/321 + -- Super specific button use case? +- https://github.com/github/primer/issues/350 + -- a11y audit with task list for PVC +- https://github.com/github/primer/issues/382 + -- React button refactor +- https://github.com/github/primer/issues/468 + -- Visual bugs (colors, state) +- https://github.com/github/primer/discussions/87 + -- Discussion about accessory buttons +- https://github.com/github/primer/discussions/211 + -- Open ended discussion about primary button as dropdown +- https://github.com/github/primer/discussions/459 + -- Allie's thoughts on limiting icon only buttons and working towards encouraging visual labels +- https://github.com/github/primer/discussions/477 + -- Buttons styled as links, links styled as buttons +- https://github.com/github/primer/issues/65 + -- Segmented control, pressed state discussion + https://github.com/primer/design/pull/193 + +## Specific notes from previous bugs to keep in mind +- [ ] Check all button variants have shadow present +- [ ] Ensure icon colors are consistent in hover states +- [ ] Ensure icon colors are consistent with variants in all states +- [ ] Ensure disabled colors are consistent across frameworks + +## Component list +A few discussions were about naming and prop drilling (source here). We found that for Button to handle all of its use cases, we would need a large number of props– some conditional and dependent on one another. When that happens, it's typically a sign that the logic should be separated into another component. + +| Component | Description | +| -- | -- | +| Button | standard `button` with variants, size, visual slots | +| IconButton | `button` with icon only (square) and required `aria-label` | +| ReactionButton | `button` snowflake with specific styles/interaction design (round) potentially a variant | +| ButtonGroup | wrapper to handle grouping buttons | +| Link | `a` with variants, optional trailing visuals | +| LinkStyledAsButton | `a` with button variants, required trailing visual | + +## Component API breakdown + +### Button + +| prop | type | options | default | notes | +| -- | -- | -- | -- | -- | +| `variant` | one-of string | `primary` `secondary` `danger` `invisible` | `secondary` | | +| `size` | one-of string | `small` `default` `large` | `default` | | +| `label` | string | button description | null | | +| `aria-label` | string | button description for screen readers | null | | +| `aria-pressed` | boolean | `true/false` | `false` | | +| `leadingVisual` | children (slot) | octicon | null | | +| `trailingVisual` | children (slot) | octicon | null | | +| `trailingAction` | children (slot) | octicon | null | slot for caret to maintain leading/trailing visuals if they exist | +| `fullWidth` | boolean | `true/false` | `false` | | +| `visualPosition` | one-of string | `fixed` `some-other-word` | `fixed` | lock icon to the text label or to the button container | + + +### LinkStyledAsButton + +| prop | type | options | default | notes | +| -- | -- | -- | -- | -- | +| `variant` | one-of string | `primary` `secondary` `danger` `invisible` | `secondary` | | +| `size` | one-of string | `small` `default` `large` | `default` | | +| `label` | string | button description | null | | +| `leadingVisual` | children (slot) | octicon | null | | +| `trailingVisual` | children (slot) | octicon | null | | +| `showTrailingAction` | arrow octicon | `true/false` | true? | show trailingVisual icon to indicate linkyness | +| `fullWidth` | boolean | `true/false` | `false` | | +| `visualPosition` | one-of string | `fixed` `some-other-word` | `fixed` | lock icon to the text label or to the button container | + +### IconButton + +| prop | type | options | default | notes | +| -- | -- | -- | -- | -- | +| `variant` | one-of string | `primary` `secondary` `danger` `outline`? | `secondary` | | +| `size` | one-of string | `small` `default` `large` | `default | | +| `aria-label` | string | button description for screen readers (required) | null | | +| `visual` | children (slot) | octicon | null | | +| `aria-pressed` | boolean | `true/false` | `false` | | + +### Link + +| prop | type | options | default | notes | +| -- | -- | -- | -- | -- | +| `variant` | one-of string | ?? we need to work on this (subtle and or muted) | | +| `label` | string | button description | null | | +| `showTrailingAction` | boolean | specific octicon for new tab or new page (or anchor?) | null | | + +### ButtonGroup + +| prop | type | options | default | notes | +| -- | -- | -- | -- | -- | +| `children` | child slot | | | | + +## Next review session +- [ ] Help refine transition animations + tokenize +- [ ] Icon colors- same as button text, or specific? +- [ ] for `LinkStyledAsButton` should `trailingAction` slot be reserved for the > chevron indicating this is a link and not a button? Or is it more of a `trailingVisual`? +- [ ] What color should `invisible` button variant be? Blue or grey? +- [ ] No aria-label for `LinkStyledAsButton` +- [ ] Do we like `fullWidth` or `block` as a prop for width behavior? +- [ ] Should `ButtonGroup` offer an option with gaps (more of a layout tool) +- [ ] Should `Button` proper handle a `pressed` (formerly selected) state, or should we create a new component `SegmentedControl` that uses `Button` and provides specific design (color, shadows)? diff --git a/src/button-refactor/button-group.scss b/src/button-refactor/button-group.scss new file mode 100644 index 0000000000..bc27caeae8 --- /dev/null +++ b/src/button-refactor/button-group.scss @@ -0,0 +1,27 @@ +.ButtonGroup { + display: flex; + flex-direction: row; +} + +// reset border-radius +.ButtonGroup-item { + border-right-width: 0; + border-radius: 0; + + &:first-of-type { + border-top-left-radius: $border-radius; + border-bottom-left-radius: $border-radius; + } + + &:last-of-type { + border-right-width: $border-width; + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; + } + + // ensure that the focus ring sits above the adjacent buttons + &:focus, + &:active { + z-index: 1; + } +} diff --git a/src/button-refactor/button.scss b/src/button-refactor/button.scss new file mode 100644 index 0000000000..8baf610001 --- /dev/null +++ b/src/button-refactor/button.scss @@ -0,0 +1,305 @@ +// temporary, pre primitives release +:root { + --primer-duration-fast: 80ms; + --primer-easing-easeInOut: cubic-bezier(0.65, 0, 0.35, 1); +} + +// base button +.Button { + position: relative; + font-size: var(--primer-text-body-size-medium); + font-weight: var(--base-text-weight-medium); + cursor: pointer; + user-select: none; + background-color: transparent; + border: var(--primer-borderWidth-thin) solid; + border-color: transparent; + border-radius: var(--primer-borderRadius-medium); + color: var(--color-btn-text); + transition: var(--primer-duration-fast) var(--primer-easing-easeInOut); + transition-property: color, fill, background-color, border-color; + text-align: center; + height: var(--primer-control-medium-size); + padding: 0 var(--primer-control-medium-paddingInline-normal); + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: var(--primer-control-medium-gap); + + // mobile friendly sizing + @media (pointer: course) { + &::before { + @include minTouchTarget(48px, 48px); + } + } + + // base states + + &:hover, + [open] > & { + transition-duration: var(--primer-duration-fast); + } + + &:active, + &.Button--active { + transition: none; + } + + &:disabled, + &.Button--disabled, + &[aria-disabled='true'] { + cursor: not-allowed; + box-shadow: none; + } + + &:focus { + @include focusOutline; + } +} + +.Button-withTooltip { + position: relative; + display: inline-block; +} + +a.Button, +summary.Button { + display: inline-flex; + + &:hover { + text-decoration: none; + } +} + +// wrap grid content to allow trailingAction to lock-right +.Button-content { + 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; + place-content: center; + // padding-bottom: 1px; // optical alignment for firefox + + > :not(:last-child) { + margin-right: var(--primer-control-medium-gap); + } +} + +// center child elements for fullWidth +.Button-content--alignStart { + justify-content: start; +} + +// button child elements + +// align svg +.Button-visual { + display: flex; + pointer-events: none; // allow click handler to work, avoiding visuals +} + +.Button-label { + grid-area: text; + white-space: nowrap; + line-height: var(--primer-text-body-lineHeight-medium); +} + +.Button-leadingVisual { + grid-area: leadingVisual; +} + +.Button-trailingVisual { + grid-area: trailingVisual; +} + +.Button-trailingAction { + margin-right: calc(var(--base-size-4) * -1); +} + +// sizes + +.Button--small { + font-size: var(--primer-text-body-size-small); + height: var(--primer-control-small-size); + padding: 0 var(--primer-control-small-paddingInline-normal); + gap: var(--primer-control-small-gap); + + .Button-label { + line-height: var(--primer-text-body-lineHeight-small); + } + + .Button-content { + > :not(:last-child) { + margin-right: var(--primer-control-small-gap); + } + } +} + +.Button--large { + height: var(--primer-control-large-size); + padding: 0 var(--primer-control-large-paddingInline-normal); + gap: var(--primer-control-large-gap); + + .Button-label { + line-height: var(--primer-text-body-lineHeight-large); + } + + .Button-content { + > :not(:last-child) { + margin-right: var(--primer-control-large-gap); + } + } +} + +.Button--fullWidth { + width: 100%; +} + +// variants + +// primary +.Button--primary { + color: var(--color-btn-primary-text); + fill: var(--color-btn-primary-icon); + background-color: var(--color-btn-primary-bg); + border-color: var(--color-btn-primary-border); + box-shadow: var(--color-btn-primary-shadow), var(--color-btn-primary-inset-shadow); + + &:hover, + [open] > & { + background-color: var(--color-btn-primary-hover-bg); + border-color: var(--color-btn-primary-hover-border); + } + + &:active, + &[aria-pressed='true'], + &.Button--pressed { + background-color: var(--color-btn-primary-selected-bg); + box-shadow: var(--color-btn-primary-selected-shadow); + } + + &:disabled, + &.Button--disabled, + &[aria-disabled='true'] { + color: var(--color-btn-primary-disabled-text); + background-color: var(--color-btn-primary-disabled-bg); + border-color: var(--color-btn-primary-disabled-border); + fill: var(--color-btn-primary-disabled-text); + } +} + +// default (secondary) +.Button--secondary { + color: var(--color-btn-text); + fill: var(--color-fg-muted); // help this + background-color: var(--color-btn-bg); + border-color: var(--color-btn-border); + box-shadow: var(--color-btn-shadow), var(--color-btn-inset-shadow); + + &:hover, + [open] > & { + background-color: var(--color-btn-hover-bg); + border-color: var(--color-btn-hover-border); + } + + &:active, + &.Button--active { + background-color: var(--color-btn-active-bg); + border-color: var(--color-btn-active-border); + } + + &[aria-pressed='true'], + &.Button--pressed { + background-color: var(--color-btn-selected-bg); + box-shadow: var(--color-primer-shadow-inset); + } + + &:disabled, + &.Button--disabled, + &[aria-disabled='true'] { + color: var(--color-primer-fg-disabled); + background-color: var(--color-btn-bg); + border-color: var(--color-btn-border); + fill: var(--color-primer-fg-disabled); + } +} + +// link color without svg +.Button--invisible { + color: var(--color-fg-default); + fill: var(--color-fg-default); + border: none; + + &:hover, + [open] > & { + background-color: var(--color-segmented-control-button-hover-bg); + } + + &[aria-pressed='true'], + &:active, + &.Button--active, + &.Button--pressed { + background-color: var(--color-segmented-control-button-active-bg); + // box-shadow: var(--color-primer-shadow-inset); + } + + &:disabled, + &.Button--disabled, + &[aria-disabled='true'] { + color: var(--color-primer-fg-disabled); + background-color: var(--color-btn-bg); + border-color: var(--color-btn-border); + fill: var(--color-primer-fg-disabled); + } + + // if visual is present, muted label color + .Button-label:not(:only-child) { + color: var(--color-btn-text); + } + + // if trailingAction is present, muted label color + .Button-content:not(:only-child) { + .Button-label { + color: var(--color-btn-text); + } + } +} + +// danger +.Button--danger { + color: var(--color-btn-danger-text); + fill: var(--color-btn-danger-icon); + background-color: var(--color-btn-bg); + border-color: var(--color-btn-border); + box-shadow: var(--color-btn-shadow), var(--color-btn-inset-shadow); + + &:hover, + [open] > & { + color: var(--color-btn-danger-hover-text); + fill: var(--color-btn-danger-hover-text); + background-color: var(--color-btn-danger-hover-bg); + border-color: var(--color-btn-danger-hover-border); + box-shadow: var(--color-btn-danger-hover-shadow), var(--color-btn-danger-hover-inset-shadow); + } + + &:active, + &[aria-pressed='true'], + &.Button--pressed { + color: var(--color-btn-danger-selected-text); + fill: var(--color-btn-danger-selected-text); + background-color: var(--color-btn-danger-selected-bg); + border-color: var(--color-btn-danger-selected-border); + box-shadow: var(--color-btn-danger-selected-shadow); + } + + &:disabled, + &.disabled, + &[aria-disabled='true'] { + color: var(--color-btn-danger-disabled-text); + fill: var(--color-btn-danger-disabled-text); + background-color: var(--color-btn-danger-disabled-bg); + border-color: var(--color-btn-border); + } +} diff --git a/src/button-refactor/icon-button.scss b/src/button-refactor/icon-button.scss new file mode 100644 index 0000000000..f2b39a5a02 --- /dev/null +++ b/src/button-refactor/icon-button.scss @@ -0,0 +1,14 @@ +.Button--iconOnly { + display: grid; + place-content: center; + padding: unset; + width: var(--primer-control-medium-size); + + &.Button--small { + width: var(--primer-control-small-size); + } + + &.Button--large { + width: var(--primer-control-large-size); + } +} diff --git a/src/button-refactor/index.scss b/src/button-refactor/index.scss new file mode 100644 index 0000000000..739c32c617 --- /dev/null +++ b/src/button-refactor/index.scss @@ -0,0 +1,6 @@ +@import '../support/index.scss'; +@import './button.scss'; +@import './button-group.scss'; +@import './icon-button.scss'; +@import './link.scss'; +@import './link-styled-as-button.scss'; diff --git a/src/button-refactor/link-styled-as-button.scss b/src/button-refactor/link-styled-as-button.scss new file mode 100644 index 0000000000..7915374118 --- /dev/null +++ b/src/button-refactor/link-styled-as-button.scss @@ -0,0 +1,75 @@ +// base link +.LinkStyledAsButton { + @extend .Button; + display: inline-flex; + + &:hover { + text-decoration: none; + } +} + +.LinkStyledAsButton-content { + @extend .Button-content; +} + +.LinkStyledAsButton-content--alignStart { + @extend .Button-content--alignStart; +} + +// link child elements + +.LinkStyledAsButton-visual { + @extend .Button-visual; +} + +.LinkStyledAsButton-label { + @extend .Button-label; +} + +.LinkStyledAsButton-leadingVisual { + @extend .Button-leadingVisual; +} + +.LinkStyledAsButton-trailingVisual { + @extend .Button-trailingVisual; +} + +.LinkStyledAsButton-trailingAction { + @extend .Button-trailingAction; +} + +// sizes + +.LinkStyledAsButton--small { + @extend .Button--small; +} + +.LinkStyledAsButton--large { + @extend .Button--large; +} + +.LinkStyledAsButton--fullWidth { + @extend .Button--fullWidth; +} + +// variants + +// primary +.LinkStyledAsButton--primary { + @extend .Button--primary; +} + +// default (secondary) +.LinkStyledAsButton--secondary { + @extend .Button--secondary; +} + +// invisible +.LinkStyledAsButton--invisible { + @extend .Button--invisible; +} + +// danger +.LinkStyledAsButton--danger { + @extend .Button--danger; +} diff --git a/src/button-refactor/link.scss b/src/button-refactor/link.scss new file mode 100644 index 0000000000..3b69f702b1 --- /dev/null +++ b/src/button-refactor/link.scss @@ -0,0 +1,28 @@ +.Link { + color: var(--color-accent-fg); + fill: var(--color-accent-fg); + + &:hover { + text-decoration: underline; + cursor: pointer; + } + + // variants + + &.Link--muted { + color: var(--color-fg-muted); + fill: var(--color-fg-muted); + + &:hover { + color: var(--color-accent-fg); + fill: var(--color-accent-fg); + } + } +} + +.Link-trailingVisual { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25em; +} diff --git a/src/core/index.scss b/src/core/index.scss index bc704ee041..ccac65d3ff 100644 --- a/src/core/index.scss +++ b/src/core/index.scss @@ -24,7 +24,6 @@ @import '../pagination/index.scss'; @import '../tooltips/index.scss'; @import '../truncate/index.scss'; -@import '../overlay/index.scss'; - +@import '../button-refactor/index.scss'; // Utilities always go last so that they can override components @import '../utilities/index.scss';