1
1
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:
simurai 2022-07-22 12:06:11 +09:00 committed by GitHub
parent ee73cf9e9a
commit 13be7ef18d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 349 additions and 2 deletions

View File

@ -0,0 +1,5 @@
---
"@primer/css": patch
---
Add `SegmentedControl` component

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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",

View File

@ -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';

View 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) &copy; [GitHub](https://github.com/)
[scss]: https://sass-lang.com/documentation/syntax#scss

View File

@ -0,0 +1,3 @@
// support files
@import '../support/index.scss';
@import './segmented-control.scss';

View 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;
}
}

View File

@ -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==