Skip to content

Commit

Permalink
Toggle switch styles (#2074)
Browse files Browse the repository at this point in the history
* 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
camertron and mperrotti authored May 23, 2022
1 parent 8354de5 commit 5cfae2c
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/smooth-lies-stare.md
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 docs/src/stories/components/ToggleSwitch/ToggleSwitch.stories.jsx
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'
}
1 change: 1 addition & 0 deletions src/product/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@
@import '../subhead/index.scss';
@import '../timeline/index.scss';
@import '../toasts/index.scss';
@import '../toggle-switch/index.scss';
2 changes: 2 additions & 0 deletions src/toggle-switch/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@import '../support/index.scss';
@import './toggle-switch.scss';
225 changes: 225 additions & 0 deletions src/toggle-switch/toggle-switch.scss
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;
}
}

0 comments on commit 5cfae2c

Please sign in to comment.