Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SegmentedControl #2083

Merged
merged 44 commits into from
Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
48b39d5
Add SegmentedControl
simurai May 18, 2022
c25185f
Fix dividers
simurai May 18, 2022
af466be
Follow Figma spec
simurai May 19, 2022
ec54b85
Create famous-moles-bow.md
simurai May 19, 2022
d2e7738
Rename item to button
simurai May 26, 2022
9d052fb
Rename visual to icon
simurai May 26, 2022
f99cab2
Rename text to label
simurai May 26, 2022
746a284
Use new size and typography tokens
simurai May 26, 2022
20f4ca5
Support IconOnly
simurai May 30, 2022
539e249
Add icon only when narrow variant
simurai May 30, 2022
2828547
Increase touch target
simurai May 31, 2022
d387489
Add disabled state
simurai May 31, 2022
856d2af
Avoid size increase when item becomes bold
simurai May 31, 2022
065151c
Add loading state
simurai May 31, 2022
4089e1a
Lint
simurai May 31, 2022
f2057fa
Add more templates
simurai Jun 1, 2022
5a522c9
Use Primitives
simurai Jun 1, 2022
f9e5a20
Merge branch 'main' into segmented-control
simurai Jun 1, 2022
2b6206a
Lint
simurai Jun 1, 2022
6db93a2
Use variable
simurai Jun 1, 2022
829a386
yarn add @primer/primitives@0.0.0-20220604151305
simurai Jun 4, 2022
c004088
Update Primitives
simurai Jun 4, 2022
49c6357
Address accessibility feedback
simurai Jun 4, 2022
46e545b
Remove loading state
simurai Jun 4, 2022
3831e4b
Rename to leadingVisual
simurai Jun 4, 2022
4ee0844
Rename label to text
simurai Jun 4, 2022
abf3153
Remove shadow
simurai Jun 4, 2022
a90d72b
Change to inset
simurai Jun 4, 2022
2994f39
Merge branch 'main' into segmented-control
simurai Jun 4, 2022
3d98ee8
Update transitions
simurai Jun 10, 2022
92e53f1
Use SegmentedControl-button--selected class instead of aria
simurai Jun 10, 2022
912039f
Remove disabled prop
simurai Jun 10, 2022
fb6807f
Keep $width-md for now
simurai Jun 10, 2022
2af922d
Add min-width
simurai Jun 10, 2022
5d1517e
Fix divider for selected item
simurai Jun 10, 2022
660aa5b
Add inset hover style
simurai Jul 20, 2022
c7077df
Keep dividers
simurai Jul 20, 2022
e3baab2
Disable hover/active state when selected
simurai Jul 20, 2022
a39eaaf
Fix a few more things
simurai Jul 20, 2022
be4188c
Lint
simurai Jul 20, 2022
70b7019
Merge branch 'main' into segmented-control
simurai Jul 20, 2022
147c07c
yarn add @primer/primitives@0.0.0-20220720082700
simurai Jul 20, 2022
b426825
yarn add @primer/primitives@^7.9.0
simurai Jul 21, 2022
9b2bf73
Merge branch 'main' into segmented-control
simurai Jul 22, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/famous-moles-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/css": patch
---

Add `SegmentedControl` component
Original file line number Diff line number Diff line change
@@ -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,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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}
>
{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>
)}
</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,
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"storybook": "cd docs && yarn && yarn storybook"
},
"dependencies": {
"@primer/primitives": "^7.8.3"
"@primer/primitives": "0.0.0-20220604151305"
},
"devDependencies": {
"@changesets/changelog-github": "0.4.4",
Expand Down
1 change: 1 addition & 0 deletions src/core/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
@import '../links/index.scss';
@import '../navigation/index.scss';
@import '../pagination/index.scss';
@import '../segmented-control/index.scss';
@import '../tooltips/index.scss';
@import '../truncate/index.scss';
@import '../overlay/index.scss';
Expand Down
25 changes: 25 additions & 0 deletions src/segmented-control/README.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions src/segmented-control/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// support files
@import '../support/index.scss';
@import './segmented-control.scss';
154 changes: 154 additions & 0 deletions src/segmented-control/segmented-control.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// 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 {
simurai marked this conversation as resolved.
Show resolved Hide resolved
position: relative;
display: inline-flex;
height: var(--primer-control-medium-size, 32px);
// stylelint-disable-next-line primer/spacing
padding: 0 var(--primer-control-medium-paddingInline-normal, 12px);
// 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);
transition: border-color, background-color var(--primer-duration-medium, 160ms) cubic-bezier(0.3, 0.1, 0.5, 1);
align-items: center;
justify-content: center;
gap: var(--primer-control-medium-gap, $spacer-2);

&:hover {
background-color: var(--color-segmented-control-button-hover-bg);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a heads up: I'm experimenting with an inset hover background in my Primer React PR (Storybook preview deployment). It's not quite right yet, but let me know what you think.

Demo:
segmented control buttons being hovered

Copy link
Contributor Author

@simurai simurai Jun 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting.. it feels a bit too much inset. But yeah, we would not need to hide the dividers when hovering, which makes it simpler.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it's too inset. I'm iterating on it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I briefly explored that hover state– wondering if it might make sense to go lighter, as to imply it will be selected (the lightest shade) on click

image

Copy link
Contributor

@mperrotti mperrotti Jun 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a good idea for this component, but we usually go darker on :hover and :active (at least for light mode).

If we went lighter, we'd probably need component-specific color tokens. If we go darker, we could share a color token between other interactive elements like ActionList items.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in our sync with @vdepizzol we decided light didn't make sense here 😄 so disregard my suggestion! I agree Mike we should try and stick with standard conventions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @langermank - I completely forgot about the discussion in our sync 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, it now uses a 3px inset padding:

2022-07-20 16 28 58

transition-duration: var(--primer-duration-fast, 80ms);
}

&:active {
background-color: var(--color-segmented-control-button-active-bg);
border-color: var(--color-segmented-control-button-active-border);
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);
}

// Remove dividers

&.SegmentedControl-button--selected::before,
&.SegmentedControl-button--selected + .SegmentedControl-button::before {
border-color: transparent;
simurai marked this conversation as resolved.
Show resolved Hide resolved
}

&:hover,
&:active,
&:focus-visible {
&::before,
+ .SegmentedControl-button::before {
border-color: transparent;
transition-duration: var(--primer-duration-fast, 80ms);
}
}
}

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

// Icon only
.SegmentedControl-button--iconOnly {
width: var(--primer-control-medium-size, 32px);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mperrotti Primer React implementation should align with this design decision.

padding-right: 0;
padding-left: 0;
}

// Icon only when narrow
@media (max-width: $width-md) {
.SegmentedControl--iconOnly-whenNarrow {
.SegmentedControl-button {
width: var(--primer-control-medium-size, 32px);
padding-right: 0;
padding-left: 0;
}

.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;
}
}
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1076,10 +1076,10 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"

"@primer/primitives@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@primer/primitives/-/primitives-7.8.3.tgz#de7e03492cf977e99f2417490d76421db9715e9f"
integrity sha512-04ZwfJhpZ0QFwDrJfCuLX6iOh0BflWDTvuoRA80lQH9xk0RtIg16INbruwwtnbSgnKKXXRSykRRJ5BbxnqufRA==
"@primer/primitives@0.0.0-20220604151305":
version "0.0.0-20220604151305"
resolved "https://registry.yarnpkg.com/@primer/primitives/-/primitives-0.0.0-20220604151305.tgz#07e430679754681c03d1f1fe607a77d41df25486"
integrity sha512-F8fybabz27G1Ekwpv5GF3F3ua+RarUnmT6ncXAT0QYmerLTBypzgL7KwHrSj7XCDw+d/AQuZbw0c/bKdEYOj4g==

"@primer/stylelint-config@^12.4.0":
version "12.6.1"
Expand Down