mirror of
https://github.com/primer/css.git
synced 2024-12-23 22:24:11 +03:00
SegmentedControl (#2083)
* Add SegmentedControl * Fix dividers * Follow Figma spec * Create famous-moles-bow.md * Rename item to button * Rename visual to icon * Rename text to label * Use new size and typography tokens * Support IconOnly * Add icon only when narrow variant * Increase touch target * Add disabled state * Avoid size increase when item becomes bold * Add loading state * Lint * Add more templates * Use Primitives * Lint * Use variable * yarn add @primer/primitives@0.0.0-20220604151305 * Update Primitives * Address accessibility feedback * Remove loading state * Rename to leadingVisual * Rename label to text * Remove shadow * Change to inset * Update transitions * Use SegmentedControl-button--selected class instead of aria * Remove disabled prop * Keep $width-md for now * Add min-width * Fix divider for selected item * Add inset hover style * Keep dividers * Disable hover/active state when selected * Fix a few more things * Lint * yarn add @primer/primitives@0.0.0-20220720082700 * yarn add @primer/primitives@^7.9.0
This commit is contained in:
parent
ee73cf9e9a
commit
13be7ef18d
5
.changeset/famous-moles-bow.md
Normal file
5
.changeset/famous-moles-bow.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@primer/css": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `SegmentedControl` component
|
@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {SegmentedControlButtonTemplate} from './SegmentedControlButton.stories' // import stories for component compositions
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/SegmentedControl',
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded'
|
||||||
|
},
|
||||||
|
excludeStories: ['BasicTemplate', 'IconsAndTextTemplate', 'IconsOnlyTemplate'],
|
||||||
|
controls: { expanded: true },
|
||||||
|
argTypes: {
|
||||||
|
ariaLabel: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Aria label',
|
||||||
|
},
|
||||||
|
fullWidth: {
|
||||||
|
control: {type: 'boolean'},
|
||||||
|
description: 'full width',
|
||||||
|
},
|
||||||
|
iconOnlyWhenNarrow: {
|
||||||
|
control: {type: 'boolean'},
|
||||||
|
description: 'icon only when narrow',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function classNames(fullWidth, iconOnlyWhenNarrow) {
|
||||||
|
const classNames = ['SegmentedControl'];
|
||||||
|
|
||||||
|
if (fullWidth) {
|
||||||
|
classNames.push("SegmentedControl--fullWidth")
|
||||||
|
}
|
||||||
|
if (iconOnlyWhenNarrow) {
|
||||||
|
classNames.push("SegmentedControl--iconOnly-whenNarrow")
|
||||||
|
}
|
||||||
|
|
||||||
|
return classNames.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BasicTemplate = ({fullWidth, ariaLabel}) => (
|
||||||
|
<>
|
||||||
|
<segmented-control role="toolbar" aria-label={ariaLabel} class={classNames(fullWidth)}>
|
||||||
|
<SegmentedControlButtonTemplate text="Outline" selected />
|
||||||
|
<SegmentedControlButtonTemplate text="Write" />
|
||||||
|
<SegmentedControlButtonTemplate text="Preview" />
|
||||||
|
<SegmentedControlButtonTemplate text="Publish" />
|
||||||
|
</segmented-control>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Basic = BasicTemplate.bind({})
|
||||||
|
Basic.args = {
|
||||||
|
ariaLabel: "Label",
|
||||||
|
fullWidth: false,
|
||||||
|
iconOnlyWhenNarrow: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconsAndTextTemplate = ({fullWidth, ariaLabel, iconOnlyWhenNarrow}) => (
|
||||||
|
<>
|
||||||
|
<segmented-control role="toolbar" aria-label={ariaLabel} class={classNames(fullWidth, iconOnlyWhenNarrow)}>
|
||||||
|
<SegmentedControlButtonTemplate text="Outline" leadingVisual />
|
||||||
|
<SegmentedControlButtonTemplate text="Write" leadingVisual selected />
|
||||||
|
<SegmentedControlButtonTemplate text="Preview" leadingVisual />
|
||||||
|
<SegmentedControlButtonTemplate text="Publish" leadingVisual />
|
||||||
|
</segmented-control>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const IconsAndText = IconsAndTextTemplate.bind({})
|
||||||
|
IconsAndText.args = {
|
||||||
|
ariaLabel: "Label",
|
||||||
|
fullWidth: false,
|
||||||
|
iconOnlyWhenNarrow: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconsOnlyTemplate = ({fullWidth, ariaLabel, iconOnlyWhenNarrow}) => (
|
||||||
|
<>
|
||||||
|
<segmented-control role="toolbar" aria-label={ariaLabel} class={classNames(fullWidth, iconOnlyWhenNarrow)}>
|
||||||
|
<SegmentedControlButtonTemplate text="Outline" leadingVisual iconOnly />
|
||||||
|
<SegmentedControlButtonTemplate text="Write" leadingVisual iconOnly />
|
||||||
|
<SegmentedControlButtonTemplate text="Preview" leadingVisual iconOnly />
|
||||||
|
<SegmentedControlButtonTemplate text="Publish" leadingVisual iconOnly selected />
|
||||||
|
</segmented-control>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const IconsOnly = IconsOnlyTemplate.bind({})
|
||||||
|
IconsOnly.args = {
|
||||||
|
ariaLabel: "Label",
|
||||||
|
fullWidth: false,
|
||||||
|
iconOnlyWhenNarrow: false,
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/SegmentedControl/SegmentedControlButton',
|
||||||
|
excludeStories: ['SegmentedControlButtonTemplate'],
|
||||||
|
layout: 'padded',
|
||||||
|
|
||||||
|
argTypes: {
|
||||||
|
selected: {
|
||||||
|
control: {type: 'boolean'},
|
||||||
|
description: 'Currently selected item',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
defaultValue: 'Item',
|
||||||
|
type: 'string',
|
||||||
|
name: 'text',
|
||||||
|
description: 'Button text',
|
||||||
|
},
|
||||||
|
leadingVisual: {
|
||||||
|
defaultValue: false,
|
||||||
|
control: {type: 'boolean'},
|
||||||
|
description: 'Has icon'
|
||||||
|
},
|
||||||
|
iconOnly: {
|
||||||
|
defaultValue: false,
|
||||||
|
control: {type: 'boolean'},
|
||||||
|
description: 'Show icon only',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// build every component case here in the template (private api)
|
||||||
|
export const SegmentedControlButtonTemplate = ({selected, text, leadingVisual, iconOnly }) => (
|
||||||
|
<>
|
||||||
|
<button className={clsx(
|
||||||
|
'SegmentedControl-button',
|
||||||
|
iconOnly && `SegmentedControl-button--iconOnly`,
|
||||||
|
selected && `SegmentedControl-button--selected`,
|
||||||
|
)}
|
||||||
|
aria-current={selected}
|
||||||
|
aria-label={iconOnly && text}
|
||||||
|
>
|
||||||
|
<div class="SegmentedControl-content">
|
||||||
|
{leadingVisual && (
|
||||||
|
<svg class="SegmentedControl-leadingVisual octicon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg>
|
||||||
|
)}
|
||||||
|
{!iconOnly && (
|
||||||
|
<span class="SegmentedControl-text" data-content={text}>{text}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
// create a "playground" demo page that may set some defaults and allow story to access component controls
|
||||||
|
export const Playground = SegmentedControlButtonTemplate.bind({})
|
||||||
|
Playground.args = {
|
||||||
|
text: 'Preview',
|
||||||
|
leadingVisual: true,
|
||||||
|
selected: true,
|
||||||
|
}
|
@ -41,7 +41,7 @@
|
|||||||
"storybook": "cd docs && yarn && yarn storybook"
|
"storybook": "cd docs && yarn && yarn storybook"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primer/primitives": "^7.8.4"
|
"@primer/primitives": "^7.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/changelog-github": "0.4.5",
|
"@changesets/changelog-github": "0.4.5",
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
@import '../links/index.scss';
|
@import '../links/index.scss';
|
||||||
@import '../navigation/index.scss';
|
@import '../navigation/index.scss';
|
||||||
@import '../pagination/index.scss';
|
@import '../pagination/index.scss';
|
||||||
|
@import '../segmented-control/index.scss';
|
||||||
@import '../tooltips/index.scss';
|
@import '../tooltips/index.scss';
|
||||||
@import '../truncate/index.scss';
|
@import '../truncate/index.scss';
|
||||||
@import '../overlay/index.scss';
|
@import '../overlay/index.scss';
|
||||||
|
25
src/segmented-control/README.md
Normal file
25
src/segmented-control/README.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
bundle: "segmented-control"
|
||||||
|
generated: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Primer CSS: `segmented-control` bundle
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Primer CSS source files are written in [SCSS]. To include this Primer CSS module in your own build, ensure that your `node_modules` directory is listed in your Sass include paths, then import it with:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
@import "@primer/css/segmented-control/index.scss";
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
The `@primer/css` npm package includes a standalone CSS build of this module in `dist/segmented-control.css`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](https://github.com/primer/css/blob/main/LICENSE) © [GitHub](https://github.com/)
|
||||||
|
|
||||||
|
|
||||||
|
[scss]: https://sass-lang.com/documentation/syntax#scss
|
3
src/segmented-control/index.scss
Normal file
3
src/segmented-control/index.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// support files
|
||||||
|
@import '../support/index.scss';
|
||||||
|
@import './segmented-control.scss';
|
159
src/segmented-control/segmented-control.scss
Normal file
159
src/segmented-control/segmented-control.scss
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
// SegmentedControl
|
||||||
|
|
||||||
|
.SegmentedControl {
|
||||||
|
display: inline-flex;
|
||||||
|
background-color: var(--color-segmented-control-bg);
|
||||||
|
// stylelint-disable-next-line primer/borders
|
||||||
|
border-radius: var(--primer-borderRadius-medium, $border-radius);
|
||||||
|
// stylelint-disable-next-line primer/box-shadow
|
||||||
|
box-shadow: var(--primer-borderInset-thin, inset 0 0 0 $border-width) var(--color-border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button -----------------------------------------
|
||||||
|
|
||||||
|
.SegmentedControl-button {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
height: var(--primer-control-medium-size, 32px);
|
||||||
|
// stylelint-disable-next-line primer/spacing
|
||||||
|
padding: calc(var(--primer-control-xsmall-paddingInline-condensed, 4px) - var(--primer-borderWidth-thin, 1px));
|
||||||
|
// stylelint-disable-next-line primer/typography
|
||||||
|
font-size: var(--primer-text-body-size-medium, $body-font-size);
|
||||||
|
color: var(--color-fg-default);
|
||||||
|
background-color: transparent;
|
||||||
|
// stylelint-disable-next-line primer/borders
|
||||||
|
border: var(--primer-borderWidth-thin, $border-width) $border-style transparent;
|
||||||
|
// stylelint-disable-next-line primer/borders
|
||||||
|
border-radius: var(--primer-borderRadius-medium, $border-radius);
|
||||||
|
|
||||||
|
&:not(.SegmentedControl-button--selected):hover .SegmentedControl-content {
|
||||||
|
background-color: var(--color-segmented-control-button-hover-bg);
|
||||||
|
transition-duration: var(--primer-duration-fast, 80ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.SegmentedControl-button--selected):active .SegmentedControl-content {
|
||||||
|
background-color: var(--color-segmented-control-button-active-bg);
|
||||||
|
transition-duration: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selected
|
||||||
|
|
||||||
|
&.SegmentedControl-button--selected {
|
||||||
|
// stylelint-disable-next-line primer/typography
|
||||||
|
font-weight: var(--base-text-weight-semibold, $font-weight-bold);
|
||||||
|
background-color: var(--color-btn-bg);
|
||||||
|
border-color: var(--color-segmented-control-button-selected-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Divider
|
||||||
|
|
||||||
|
// stylelint-disable-next-line scss/selector-no-redundant-nesting-selector
|
||||||
|
& + .SegmentedControl-button::before {
|
||||||
|
position: absolute;
|
||||||
|
inset: var(--primer-borderWidth-thin, 1px) 0 0 calc(var(--primer-borderWidth-thin, 1px) * -1);
|
||||||
|
height: var(--primer-text-body-size-large, 16px);
|
||||||
|
// stylelint-disable-next-line primer/spacing
|
||||||
|
margin-top: var(--primer-control-medium-paddingBlock, 6px);
|
||||||
|
content: '';
|
||||||
|
// stylelint-disable-next-line primer/borders
|
||||||
|
border-left: var(--primer-borderWidth-thin, $border-width) $border-style var(--color-border-default);
|
||||||
|
transition: border-color var(--primer-duration-medium, 160ms) cubic-bezier(0.3, 0.1, 0.5, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.SegmentedControl-button--selected::before,
|
||||||
|
&.SegmentedControl-button--selected + .SegmentedControl-button::before {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content -----------------------------------------
|
||||||
|
|
||||||
|
.SegmentedControl-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--primer-control-medium-gap, $spacer-2);
|
||||||
|
height: 100%;
|
||||||
|
// stylelint-disable-next-line primer/spacing
|
||||||
|
padding: 0 var(--primer-control-medium-paddingInline-condensed, 8px);
|
||||||
|
// stylelint-disable-next-line primer/borders
|
||||||
|
border-radius: var(--primer-borderRadius-medium, $border-radius);
|
||||||
|
transition: background-color var(--primer-duration-medium, 160ms) cubic-bezier(0.3, 0.1, 0.5, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leading visual -----------------------------------------
|
||||||
|
|
||||||
|
.SegmentedControl-leadingVisual {
|
||||||
|
color: var(--color-fg-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text -----------------------------------------
|
||||||
|
|
||||||
|
.SegmentedControl-text {
|
||||||
|
// renders a visibly hidden "copy" of the text in bold, reserving box space for when text becomes bold on selected
|
||||||
|
&[data-content]::before {
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
// stylelint-disable-next-line primer/typography
|
||||||
|
font-weight: var(--base-text-weight-semibold, $font-weight-bold);
|
||||||
|
visibility: hidden;
|
||||||
|
content: attr(data-content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variants -----------------------------------------
|
||||||
|
|
||||||
|
// fullWidth
|
||||||
|
.SegmentedControl--fullWidth {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.SegmentedControl-button {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon only
|
||||||
|
.SegmentedControl-button--iconOnly {
|
||||||
|
width: var(--primer-control-medium-size, 32px);
|
||||||
|
|
||||||
|
.SegmentedControl-content {
|
||||||
|
padding: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon only when narrow
|
||||||
|
@media (max-width: $width-md) {
|
||||||
|
.SegmentedControl--iconOnly-whenNarrow {
|
||||||
|
.SegmentedControl-button {
|
||||||
|
width: var(--primer-control-medium-size, 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.SegmentedControl-content {
|
||||||
|
padding: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SegmentedControl-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase touch target
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
.SegmentedControl-button {
|
||||||
|
min-width: var(--primer-control-minTarget-coarse, 44px);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@include minTouchTarget($min-height: var(--primer-control-minTarget-coarse, 44px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset for icon-only buttons
|
||||||
|
.SegmentedControl-button--iconOnly,
|
||||||
|
.SegmentedControl--iconOnly-whenNarrow .SegmentedControl-button {
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
}
|
@ -1091,7 +1091,7 @@
|
|||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
"@primer/primitives@^7.8.4":
|
"@primer/primitives@^7.9.0":
|
||||||
version "7.9.0"
|
version "7.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@primer/primitives/-/primitives-7.9.0.tgz#c8a27287488c8308b1715a7d73214629c331544a"
|
resolved "https://registry.yarnpkg.com/@primer/primitives/-/primitives-7.9.0.tgz#c8a27287488c8308b1715a7d73214629c331544a"
|
||||||
integrity sha512-ZHHfwB0z0z6nDJp263gyGIClYDy+rl0nwqyi4qhcv3Cxhkmtf+If2KVjr6FQqBBFfi1wQwUzaax2FBvfEMFBnw==
|
integrity sha512-ZHHfwB0z0z6nDJp263gyGIClYDy+rl0nwqyi4qhcv3Cxhkmtf+If2KVjr6FQqBBFfi1wQwUzaax2FBvfEMFBnw==
|
||||||
|
Loading…
Reference in New Issue
Block a user