-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add styles for the ToggleSwitch component * Add changeset * Disable stylelint temporarily; commit stylelint fixes * Refactor CSS; add stories * Clean up; rename ToggleSwitch-switch to ToggleSwitch-track * Address a bunch of PR feedback: * Rename ToggleSwitch-bg -> ToggleSwitch-icons * Rename ToggleSwitch-label -> ToggleSwitch-status * Collapse border-* styles into a single border-style property * Replace CSS with touch target @include * Remove unnecessary :after pseudo-element * Remove sr-only span, as it's redundant * Collapse border-* properties again (stylelint didn't like argument order last time) Co-authored-by: Mike Perrotti <mperrotti@github.com>
- Loading branch information
Showing
5 changed files
with
337 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@primer/css": minor | ||
--- | ||
|
||
Add styles for the ToggleSwitch component |
104 changes: 104 additions & 0 deletions
104
docs/src/stories/components/ToggleSwitch/ToggleSwitch.stories.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import React from 'react' | ||
|
||
export default { | ||
title: 'Components/ToggleSwitch', | ||
parameters: { | ||
layout: 'padded' | ||
}, | ||
excludeStories: ['ToggleSwitchTemplate'], | ||
controls: { expanded: true }, | ||
argTypes: { | ||
checked: { | ||
control: {type: 'boolean'}, | ||
description: 'checkbox state' | ||
}, | ||
disabled: { | ||
description: 'disabled field', | ||
control: {type: 'boolean'} | ||
}, | ||
size: { | ||
options: ['medium', 'small'], | ||
control: { | ||
type: 'inline-radio' | ||
}, | ||
description: 'size' | ||
}, | ||
labelPosition: { | ||
options: ['start', 'end'], | ||
control: { | ||
type: 'inline-radio' | ||
}, | ||
description: 'label position' | ||
} | ||
} | ||
} | ||
|
||
function classNamesForSwitch(disabled, checked, size, labelPosition) { | ||
const classNames = ['ToggleSwitch']; | ||
|
||
if (checked) { | ||
classNames.push("ToggleSwitch--checked") | ||
} | ||
if (disabled) { | ||
classNames.push("ToggleSwitch--disabled") | ||
} | ||
if (size === 'small') { | ||
classNames.push("ToggleSwitch--small") | ||
} | ||
if (labelPosition === 'end') { | ||
classNames.push('ToggleSwitch--statusAtEnd') | ||
} | ||
|
||
return classNames.join(' ') | ||
} | ||
|
||
export const ToggleSwitchTemplate = ({disabled, checked, size, labelPosition}) => ( | ||
<> | ||
<toggle-switch class={classNamesForSwitch(disabled, checked, size, labelPosition)}> | ||
<span aria-hidden="true" className="ToggleSwitch-status"> | ||
<div className="ToggleSwitch-statusOn" style={{visibility: checked ? 'visible' : 'hidden' }}>On</div> | ||
<div className="ToggleSwitch-statusOff" style={{visibility: checked ? 'hidden' : 'visible' }}>Off</div> | ||
</span> | ||
|
||
<button | ||
className="ToggleSwitch-track" | ||
role="switch" | ||
aria-checked={checked ? 'true' : 'false'} | ||
aria-disabled={disabled ? "true" : "false"}> | ||
<div className="ToggleSwitch-icons" aria-hidden="true"> | ||
<div className="ToggleSwitch-lineIcon"> | ||
<svg | ||
width={size === 'small' ? 12 : 16} | ||
height={size === 'small' ? 12 : 16} | ||
viewBox="0 0 16 16" | ||
fill="currentColor" | ||
xmlns="http://www.w3.org/2000/svg"> | ||
<path fill-rule="evenodd" d="M8 2a.75.75 0 0 1 .75.75v11.5a.75.75 0 0 1-1.5 0V2.75A.75.75 0 0 1 8 2Z" /> | ||
</svg> | ||
</div> | ||
|
||
<div className="ToggleSwitch-circleIcon"> | ||
<svg | ||
width={size === 'small' ? 12 : 16} | ||
height={size === 'small' ? 12 : 16} | ||
viewBox="0 0 16 16" | ||
fill="currentColor" | ||
xmlns="http://www.w3.org/2000/svg"> | ||
<path fill-rule="evenodd" d="M8 12.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9ZM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12Z" /> | ||
</svg> | ||
</div> | ||
</div> | ||
|
||
<div className="ToggleSwitch-knob" /> | ||
</button> | ||
</toggle-switch> | ||
</> | ||
) | ||
|
||
export const Playground = ToggleSwitchTemplate.bind({}) | ||
Playground.args = { | ||
disabled: false, | ||
checked: false, | ||
size: 'medium', | ||
labelPosition: 'start' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
@import '../support/index.scss'; | ||
@import './toggle-switch.scss'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
.ToggleSwitch { | ||
align-items: center; | ||
display: inline-flex; | ||
gap: $spacer-2; | ||
|
||
&:hover { | ||
.ToggleSwitch-knob { | ||
background-color: var(--color-btn-hover-bg); | ||
} | ||
} | ||
|
||
&:active { | ||
.ToggleSwitch-knob { | ||
background-color: var(--color-btn-active-bg); | ||
} | ||
} | ||
} | ||
|
||
.ToggleSwitch-track { | ||
position: relative; | ||
display: block; | ||
width: $spacer-8; | ||
height: $spacer-5; | ||
padding: 0; | ||
overflow: hidden; | ||
text-decoration: none; | ||
cursor: pointer; | ||
user-select: none; | ||
background-color: var(--color-switch-track-bg); | ||
border: $border-width $border-style var(--color-switch-track-border); | ||
border-radius: $border-radius; | ||
transition-timing-function: cubic-bezier(0.5, 1, 0.89, 1); | ||
transition-duration: 80ms; | ||
transition-property: background-color, border-color; | ||
appearance: none; | ||
|
||
&:focus, | ||
&:focus-visible { | ||
outline-offset: 0; | ||
} | ||
|
||
@media (pointer: coarse) { | ||
&::before { | ||
@include minTouchTarget(calc($spacer-6 + $spacer-1)); | ||
} | ||
} | ||
|
||
@media (prefers-reduced-motion) { | ||
transition: none; | ||
|
||
* { | ||
transition: none; | ||
} | ||
} | ||
} | ||
|
||
.ToggleSwitch-track[aria-checked='true'][aria-disabled='true'] { | ||
background-color: var(--color-canvas-subtle); | ||
border-color: var(--color-border-subtle); | ||
|
||
&:hover, | ||
&:active { | ||
background-color: var(--color-canvas-subtle); | ||
|
||
// This is the most straightforward way of setting the knob's styles when the | ||
// switch is both checked and disabled. | ||
|
||
// stylelint-disable-next-line selector-max-specificity | ||
.ToggleSwitch-knob { | ||
background-color: var(--color-switch-knob-checked-disabled-bg); | ||
} | ||
} | ||
|
||
.ToggleSwitch-knob { | ||
background-color: var(--color-switch-knob-checked-disabled-bg); | ||
} | ||
} | ||
|
||
.ToggleSwitch-track[aria-checked='true'] { | ||
background-color: var(--color-switch-track-checked-bg); | ||
border-color: var(--color-switch-track-checked-border); | ||
|
||
&:hover { | ||
background-color: var(--color-switch-track-checked-hover-bg); | ||
} | ||
|
||
&:active { | ||
background-color: var(--color-switch-track-checked-active-bg); | ||
} | ||
|
||
.ToggleSwitch-knob { | ||
background-color: var(--color-switch-knob-checked-bg); | ||
border: 0; | ||
transform: translateX(calc(100% + 1px)); | ||
} | ||
|
||
.ToggleSwitch-lineIcon { | ||
transform: translateX(0%); | ||
} | ||
|
||
.ToggleSwitch-circleIcon { | ||
transform: translateX(100%); | ||
} | ||
} | ||
|
||
.ToggleSwitch-track[aria-disabled='true'] { | ||
cursor: not-allowed; | ||
background-color: var(--color-canvas-subtle); | ||
border-color: var(--color-border-subtle); | ||
transition-property: none; | ||
|
||
&:hover, | ||
&:active { | ||
.ToggleSwitch-knob { | ||
background-color: var(--color-btn-bg); | ||
} | ||
} | ||
|
||
.ToggleSwitch-knob { | ||
border-color: var(--color-border-default); | ||
box-shadow: none; | ||
|
||
&:hover, | ||
&:active { | ||
background-color: var(--color-btn-bg); | ||
} | ||
} | ||
|
||
.ToggleSwitch-lineIcon { | ||
color: var(--color-fg-subtle); | ||
} | ||
|
||
.ToggleSwitch-circleIcon { | ||
color: var(--color-fg-subtle); | ||
} | ||
} | ||
|
||
.ToggleSwitch-icons { | ||
display: flex; | ||
align-items: center; | ||
width: 100%; | ||
height: 100%; | ||
overflow: hidden; | ||
} | ||
|
||
.ToggleSwitch-lineIcon { | ||
line-height: 0; | ||
color: var(--color-accent-fg); | ||
transition-duration: 80ms; | ||
transition-property: transform; | ||
transform: translateX(-100%); | ||
flex: 1 0 50%; | ||
} | ||
|
||
.ToggleSwitch-circleIcon { | ||
line-height: 0; | ||
color: var(--color-fg-default); | ||
transition-duration: 80ms; | ||
transition-property: transform; | ||
transform: translateX(0); | ||
flex: 1 0 50%; | ||
} | ||
|
||
.ToggleSwitch-knob { | ||
position: absolute; | ||
top: -1px; | ||
bottom: -1px; | ||
z-index: 1; | ||
width: 50%; | ||
background-color: var(--color-btn-bg); | ||
border: $border-width $border-style var(--color-switch-track-border); | ||
border-radius: $border-radius; | ||
box-shadow: var(--color-shadow-medium), var(--color-btn-inset-shadow); | ||
transition-timing-function: cubic-bezier(0.5, 1, 0.89, 1); | ||
transition-duration: 80ms; | ||
transition-property: transform; | ||
transform: translateX(-1px); | ||
|
||
@media (prefers-reduced-motion) { | ||
transition: none; | ||
} | ||
} | ||
|
||
.ToggleSwitch-status { | ||
position: relative; | ||
font-size: $body-font-size; | ||
line-height: $body-line-height; | ||
color: var(--color-fg-default); | ||
text-align: right; | ||
} | ||
|
||
.ToggleSwitch--small { | ||
.ToggleSwitch-status { | ||
font-size: $font-size-small; | ||
} | ||
|
||
.ToggleSwitch-track { | ||
width: $spacer-7; | ||
height: $spacer-4; | ||
} | ||
} | ||
|
||
.ToggleSwitch--disabled { | ||
.ToggleSwitch-status { | ||
color: var(--color-fg-muted); | ||
} | ||
} | ||
|
||
.ToggleSwitch-statusOn { | ||
height: 0; | ||
visibility: hidden; | ||
} | ||
|
||
.ToggleSwitch-statusOff { | ||
height: auto; | ||
visibility: visible; | ||
} | ||
|
||
.ToggleSwitch--statusAtEnd { | ||
flex-direction: row-reverse; | ||
|
||
.ToggleSwitch-status { | ||
text-align: left; | ||
} | ||
} |