Skip to content

Commit

Permalink
Switch: New component contribution (#825)
Browse files Browse the repository at this point in the history
* feat(switch): add toggle switch

* feat(switch): clean up tests and storybook stories

* feat(switch): update color palette, fix stories

* feat(switch): update spacing and cursors

* feat(switch): update colors to be accessible

* feat(switch): add change event to storybook and docs

* Update packages/pharos-site/static/guidelines/switch.docs.tsx

Co-authored-by: Brent Swisher <[email protected]>

* feat(switch): sizing and styling

* docs(switch): add to site navigation

* docs(switch): register new component in pharos-site

* feat(switch): re-add font weight

* feat(switch): fix transition timing

* feat(switch): use inset border increase and update outline offset

---------

Co-authored-by: Brent Swisher <[email protected]>
Co-authored-by: Brent Swisher <[email protected]>
  • Loading branch information
3 people authored Oct 31, 2024
1 parent 62a6b66 commit fc902f2
Show file tree
Hide file tree
Showing 15 changed files with 741 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .changeset/strange-planes-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@ithaka/pharos-site': minor
'@ithaka/pharos': minor
---

Add switch component
2 changes: 2 additions & 0 deletions .storybook/initComponents.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
PharosSidenavLink,
PharosSidenavMenu,
PharosSidenavSection,
PharosSwitch,
PharosTabs,
PharosTab,
PharosTable,
Expand Down Expand Up @@ -85,6 +86,7 @@ registerComponents('storybook', [
PharosSidenavLink,
PharosSidenavMenu,
PharosSidenavSection,
PharosSwitch,
PharosTabs,
PharosTab,
PharosTable,
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
nodejs 20.11.1
nodejs 20.18.0
1 change: 1 addition & 0 deletions packages/pharos-site/initComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ if (typeof window !== `undefined`) {
pharos.PharosSidenavLink,
pharos.PharosSidenavMenu,
pharos.PharosSidenavSection,
pharos.PharosSwitch,
pharos.PharosTabs,
pharos.PharosTab,
pharos.PharosTable,
Expand Down
1 change: 1 addition & 0 deletions packages/pharos-site/src/components/Sidenav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ const Sidenav: FC<SidenavProps> = ({ isOpen, showCloseButton }) => {
'Radio button',
'Select',
'Sidenav',
'Switch',
'Tabs',
'Toast',
'Tooltip',
Expand Down
1 change: 1 addition & 0 deletions packages/pharos-site/src/pages/components/switch/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as default } from '@guidelines/switch.docs';
172 changes: 172 additions & 0 deletions packages/pharos-site/static/guidelines/switch.docs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import PageSection from '@components/statics/PageSection.tsx';
import BestPractices from '@components/statics/BestPractices.tsx';
import { PharosSwitch, PharosHeading, PharosLink } from '@ithaka/pharos/lib/react-components';
import Canvas from '../../src/components/Canvas';
import { FC } from 'react';

const SwitchPage: FC = () => {
return (
<>
<PageSection
title="Switch"
description="Switch are stylized toggles, similar to checkboxes, to indicate a boolean value."
isHeader
storyBookType="forms"
>
<PharosSwitch>
<span slot="label">I am a switch</span>
</PharosSwitch>
</PageSection>
<PageSection
topMargin
title="Usage"
description="The switch component is used to toggle if a control is enabled or disabled. The value of the switch is persisted at the moment the control is updated, without a separate save mechanism."
>
<PageSection title="Alignment" subSectionLevel={1}>
<p>Labels for switches are placed to the left of the switch.</p>
</PageSection>
<PageSection title="Placement" subSectionLevel={1}>
<p>
Switches in JSTOR are often used when enabling or disabling features on the site. Their
larger size and clear on/off state make them easy to use and understand.
</p>
</PageSection>
</PageSection>{' '}
<PageSection title="Best practices">
<BestPractices
Do={
<ul>
<li>
Use switches when users need to choose "yes" or "no" per option, with no
indeterminate state
</li>
<li>
Switches should work independently from each other, unless there are switches to
control groups
</li>
<li>Switches should always include a label</li>
<li>Use switches when choices are not mutually exclusive</li>
<li>The list of choices should be in a logical order</li>
<li>Value of the switch should be persisted when the control is changed</li>
</ul>
}
Dont={
<ul>
<li>Don't use switches when there are no choices or a choice is non-binary</li>
<li>
Don't use where only one choice in a group is allowed. Consider using the radio
button component instead
</li>
<li>
Don't make switches affect other switches unless there is a clear hierarchy of
controls (aka master switch and sub-switches)
</li>
</ul>
}
/>
</PageSection>{' '}
<PageSection title="Content guidelines">
<PageSection title="Labels" subSectionLevel={1}>
<ul>
<li>
Labels are descriptive and succinct. They should provide further clarity for the user.
</li>
<li>Labels should not end in punctuation.</li>
<li>Use Sentence case for labels.</li>
<li>
Avoid using negative language as it can be counterintuitive. For example, "I agree to
the terms" instead of "I don't agree to the terms."
</li>
<li>
Long labels may wrap to a second line, but consider rewording the label if it gets too
long.
</li>
<li>Labels should not be truncated.</li>
</ul>
</PageSection>
</PageSection>{' '}
<PageSection title="States">
<PageSection title="Default" subSectionLevel={1}>
<p>Indicates that the switch is interactable.</p>
<Canvas>
<PharosSwitch>
<span slot="label">Switch label</span>
</PharosSwitch>
</Canvas>
</PageSection>
<PageSection title="Disabled" subSectionLevel={1}>
<p>Indicates that the switch should not be interactable.</p>
<Canvas>
<PharosSwitch disabled>
<span slot="label">Switch label</span>
</PharosSwitch>
</Canvas>
</PageSection>
<PageSection title="Checked" subSectionLevel={1}>
<p>Indicates that the switch is selected and will submit a checked value of true.</p>
<Canvas>
<PharosSwitch checked>
<span slot="label">Switch label</span>
</PharosSwitch>
</Canvas>
</PageSection>
<PageSection title="Unchecked" subSectionLevel={1}>
<p>
Indicates that the switch is not selected and will submit an unchecked value of false.
</p>
<Canvas>
<PharosSwitch>
<span slot="label">Switch label</span>
</PharosSwitch>
</Canvas>
</PageSection>
</PageSection>
<PageSection title="Accessibility">
<PageSection subSectionLevel={1} title="Relevant WCAG guidelines">
<ul>
<li>
<PharosLink href="https://www.w3.org/WAI/WCAG21/Understanding/name-role-value">
4.1.2 Name, Role, Value A
</PharosLink>
</li>
</ul>
</PageSection>
<PageSection subSectionLevel={1} title="Importance">
Switches help users to understand the relationship between selections in a form element or
filter. A switch grouping also will typically allow users to select more than one item in
the grouping.
</PageSection>
<PageSection subSectionLevel={1} title="Code expectations">
<ul>
<li> The switch has the role "switch". </li>
<li>
When the switch is selected, the ARIA state is set to <code>aria-checked="true"</code>{' '}
and when it is deselected <code>aria-checked="false"</code>.
</li>
</ul>
</PageSection>
<PageSection subSectionLevel={1} title="Expected actions">
<PageSection subSectionLevel={2} title="Screen reader">
<ul>
<li>Reads: "item name, state (checked or unchecked), switch, group" </li>
</ul>
</PageSection>
<PageSection subSectionLevel={2} title="Keyboard">
<ul>
<li>
The <kbd>Space</kbd> key can be used to select and deselect each switch when it has
focus.
</li>
<li>
Users can navigate between switch inputs by pressing <kbd>Tab</kbd> or{' '}
<kbd>Shift</kbd>-<kbd>Tab</kbd>.
</li>
<li>Switches identified as `disabled` attribute are ignored in the tab order.</li>
</ul>
</PageSection>
</PageSection>
</PageSection>
</>
);
};
export default SwitchPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { PharosSwitch } from '../../react-components/switch/pharos-switch';
import { configureDocsPage } from '@config/docsPageConfig';
import { PharosContext } from '../../utils/PharosContext';
import { action } from '@storybook/addon-actions';

export default {
title: 'Forms/Switch',
component: PharosSwitch,
decorators: [
(Story) => (
<PharosContext.Provider value={{ prefix: 'storybook' }}>
<Story />
</PharosContext.Provider>
),
],
parameters: {
docs: {
page: configureDocsPage('switch'),
},
options: { selectedPanel: 'addon-controls' },
},
};

export const Base = {
render: ({ disabled, checked }) => (
<PharosSwitch
disabled={disabled}
checked={checked}
onChange={(e) => action('Change')(e.target.checked)}
>
<span slot="label">Toggle Switch</span>
</PharosSwitch>
),
args: {
disabled: false,
checked: false,
},
parameters: {
options: { selectedPanel: 'addon-actions' },
},
};
123 changes: 123 additions & 0 deletions packages/pharos/src/components/switch/pharos-switch.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
@use '../../utils/scss/mixins';

.input-wrapper {
@include mixins.option-wrapper;

align-items: center;
column-gap: var(--pharos-spacing-1-x);
}

.switch__control {
height: 32px;
width: 70px;
display: flex;
align-items: center;
box-sizing: border-box;
cursor: pointer;
border: 1px solid;
border-color: var(--pharos-color-marble-gray-80);
border-radius: 2rem;
background-color: var(--pharos-color-marble-gray-94);
transition:
background-color var(--pharos-transition-duration-default) ease-out,
border-color var(--pharos-transition-duration-default) ease-out,
box-shadow var(--pharos-transition-duration-default) ease-out;
position: relative;

&:hover {
border-color: var(--pharos-color-marble-gray-30);
box-shadow: inset 0 0 0 1px var(--pharos-color-marble-gray-30);
}

&::before {
position: absolute;
content: '';
height: 1rem;
width: 1rem;
background-color: var(--pharos-color-marble-gray-30);
left: 9px;
transition: var(--pharos-transition-duration-default) ease-out;
border-radius: 50%;
}

&::after {
font-weight: var(--pharos-font-weight-bold);
color: var(--pharos-color-marble-gray-30);
content: 'OFF';
position: absolute;
right: 9px;
transition: color var(--pharos-transition-duration-default) ease-out;
font-size: var(--pharos-font-size-small);
}

.switch__input:checked + .input-wrapper & {
background-color: var(--pharos-color-green-97);
border-color: var(--pharos-color-green-base);

&:hover {
box-shadow: inset 0 0 0 1px var(--pharos-color-green-base);
}

&::before {
background-color: var(--pharos-color-green-base);
left: calc(100% - 9px - 1rem);
}

&::after {
color: var(--pharos-color-green-base);
content: 'ON';
left: 9px;
right: unset;
}
}
}

.switch__label {
cursor: pointer;
}

#switch-element {
@include mixins.option-input;
}

#switch-element:focus-visible {
+ .input-wrapper .switch__control {
outline: 2px solid var(--pharos-color-focus);
outline-offset: 2px;
border-color: var(--pharos-color-marble-gray-30);
box-shadow: inset 0 0 0 1px var(--pharos-color-marble-gray-30);
}
}

#switch-element:disabled {
+ .input-wrapper .switch__label {
cursor: default;
}

+ .input-wrapper .switch__control {
cursor: default;
background-color: var(--pharos-color-marble-gray-base);
border-color: var(--pharos-color-marble-gray-94);

&:hover {
box-shadow: none;
}

&::before {
background-color: var(--pharos-color-marble-gray-50);
}

&::after {
color: var(--pharos-color-marble-gray-50);
}
}
}

#switch-element:focus-visible:checked {
+ .input-wrapper .switch__control {
outline: 2px solid var(--pharos-color-focus);
outline-offset: 2px;
border-color: var(--pharos-color-green-base);
box-shadow: inset 0 0 0 1px var(--pharos-color-green-base);
}
}
Loading

0 comments on commit fc902f2

Please sign in to comment.