Skip to content

Commit

Permalink
Add ToggleSwitch component (#1933)
Browse files Browse the repository at this point in the history
* adds Switch component and stories

* adds tests and more ARIA markup

* adds component docs and fixes transition

* adds changeset

* update snapshots

* addresses component design feedback

* improves docs and storybook examples

* updates snaps

* Update src/Switch.tsx

Co-authored-by: Cole Bemis <colebemis@github.com>

* Update docs/content/Switch.mdx

Co-authored-by: Cole Bemis <colebemis@github.com>

* Update src/Switch.tsx

Co-authored-by: Cole Bemis <colebemis@github.com>

* Update src/Switch.tsx

Co-authored-by: Cole Bemis <colebemis@github.com>

* addresses PR feedback

* increases contrast of control to meet WCAG guidelines

* CSS cleanup and revert to light off button

* updates snapshots

* addresses PR feedback

* rename switch docs

* use component primitives for toggle switch colors

* fixes documentation mistakes

* updates snapshots

* Update docs/content/ToggleSwitch.mdx

Co-authored-by: Cole Bemis <colebemis@github.com>

* Update docs/content/ToggleSwitch.mdx

Co-authored-by: Cole Bemis <colebemis@github.com>

* updates themePreval snapshot

* upgrades primitives, updates snapshots

* Update snapshot

Co-authored-by: Cole Bemis <colebemis@github.com>
Co-authored-by: simurai <simulus@gmail.com>
  • Loading branch information
3 people committed Mar 24, 2022
1 parent adbcd3b commit ae7650f
Show file tree
Hide file tree
Showing 15 changed files with 1,342 additions and 47 deletions.
5 changes: 5 additions & 0 deletions .changeset/stupid-carrots-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Adds a toggle switch component
220 changes: 220 additions & 0 deletions docs/content/ToggleSwitch.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
---
componentId: toggle_switch
title: ToggleSwitch
description: Toggles a setting on or off, and immediately saves the change
status: Alpha
source: https://github.com/primer/react/blob/main/src/ToggleSwitch.tsx
storybook: '/react/storybook?path=/story/toggleswitch-examples--default'
---

## Examples

### Basic

```jsx live
<Box display="flex" maxWidth="300px">
<Box flexGrow={1} fontSize={2} fontWeight="bold" id="switchLabel">
Notifications
</Box>
<ToggleSwitch aria-labelledby="switchLabel" />
</Box>
```

### Uncontrolled with default value

```jsx live
<Box display="flex" maxWidth="300px">
<Box flexGrow={1} fontSize={2} fontWeight="bold" id="switchLabel">
Notifications
</Box>
<ToggleSwitch defaultChecked aria-labelledby="switchLabel" />
</Box>
```

### Controlled

```javascript noinline live
const Controlled = () => {
const [isOn, setIsOn] = React.useState(false)

const onClick = () => {
setIsOn(!isOn)
}

const handleSwitchChange = on => {
console.log(`new switch "on" state: ${on}`)
}

return (
<>
<Box display="flex" maxWidth="300px">
<Box flexGrow={1} fontSize={2} fontWeight="bold" id="switchLabel">
Notifications
</Box>
<ToggleSwitch onClick={onClick} onChange={handleSwitchChange} checked={isOn} aria-labelledby="switchLabel" />
</Box>
<p>The switch is {isOn ? 'on' : 'off'}</p>
</>
)
}

render(Controlled)
```

### Small

```jsx live
<Box display="flex" maxWidth="300px">
<Box flexGrow={1} fontSize={1} fontWeight="bold" id="switchLabel">
Notifications
</Box>
<ToggleSwitch aria-labelledby="switchLabel" size="small" />
</Box>
```

### Delayed toggle with loading state

```javascript noinline live
const LoadingToggle = () => {
const [loading, setLoading] = React.useState(false)
const [isOn, setIsOn] = React.useState(false)

async function switchSlowly(currentOn) {
await new Promise(resolve => setTimeout(resolve, 1500))
return await !currentOn
}

async function onClick() {
setLoading(!loading)
const newSwitchState = await switchSlowly(isOn)
setIsOn(newSwitchState)
}

const handleSwitchChange = React.useCallback(
on => {
setLoading(false)
},
[setLoading]
)

return (
<>
<Box display="flex" maxWidth="300px">
<Box flexGrow={1} fontSize={2} fontWeight="bold" id="switchLabel">
Notifications
</Box>
<ToggleSwitch
aria-labelledby="switchLabel"
loading={loading}
checked={isOn}
onClick={onClick}
onChange={handleSwitchChange}
/>
</Box>
<p>The switch is {isOn ? 'on' : 'off'}</p>
</>
)
}

render(LoadingToggle)
```

### Disabled

```jsx live
<Box display="flex" maxWidth="300px">
<Box flexGrow={1} fontSize={2} fontWeight="bold" id="switchLabel">
Notifications
</Box>
<ToggleSwitch aria-labelledby="switchLabel" disabled />
</Box>
```

### With associated caption text

```jsx live
<Box display="flex">
<Box flexGrow={1}>
<Text fontSize={2} fontWeight="bold" id="switchLabel" display="block">
Notifications
</Text>
<Text color="fg.subtle" fontSize={1} id="switchCaption" display="block">
Notifications will be delivered via email and the GitHub notification center
</Text>
</Box>
<ToggleSwitch aria-labelledby="switchLabel" aria-describedby="switchCaption" />
</Box>
```

### Left-aligned with label

```jsx live
<>
<Text fontSize={2} fontWeight="bold" id="switchLabel" display="block" mb={1}>
Notifications
</Text>
<ToggleSwitch statusLabelPosition="end" aria-labelledby="switchLabel" />
</>
```

## Props

<PropsTable>
<PropsTableRow name="aria-describedby" type="string" description="The id of the DOM node that describes the switch" />
<PropsTableRow
name="aria-labelledby"
type="string"
required
description="The id of the DOM node that labels the switch"
/>
<PropsTableRow name="defaultChecked" type="boolean" description="Uncontrolled - whether the switch is turned on" />
<PropsTableRow name="disabled" type="boolean" description="Whether the switch is ready for user input" />
<PropsTableRow name="loading" type="boolean" description="Whether the switch's value is being calculated" />
<PropsTableRow name="checked" type="boolean" description="Whether the switch is turned on" />
<PropsTableRow
name="onChange"
type="(on: boolean) => void"
description="The callback that is called when the switch is toggled on or off"
/>
<PropsTableRow
name="onClick"
type="(e: MouseEvent) => void"
description="The callback that is called when the switch is clicked"
/>
<PropsTableRow name="size" type="'small' | 'medium'" defaultValue="'medium'" description="Size of the switch" />
<PropsTableRow
name="statusLabelPosition"
type="'start' | 'end'"
defaultValue="'start'"
description={
<>
<div>Whether the "on" and "off" labels should appear before or after the switch.</div>
<div>
<Text fontWeight="bold">This should only be changed when the switch's alignment needs to be adjusted.</Text>{' '}
For example: It needs to be left-aligned because the label appears above it and the caption appears below it.
</div>
</>
}
/>
</PropsTable>

## Status

<ComponentChecklist
items={{
propsDocumented: true,
noUnnecessaryDeps: true,
adaptsToThemes: true,
adaptsToScreenSizes: true,
fullTestCoverage: true,
usedInProduction: false,
usageExamplesDocumented: true,
hasStorybookStories: true,
designReviewed: false,
a11yReviewed: false,
stableApi: false,
addressedApiFeedback: false,
hasDesignGuidelines: false,
hasFigmaComponent: false
}}
/>
2 changes: 2 additions & 0 deletions docs/src/@primer/gatsby-theme-doctocat/nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@
url: /StyledOcticon
- title: SubNav
url: /SubNav
- title: ToggleSwitch
url: /ToggleSwitch
- title: TabNav
url: /TabNav
- title: Textarea
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"dependencies": {
"@primer/behaviors": "1.1.0",
"@primer/octicons-react": "16.1.1",
"@primer/primitives": "7.5.1",
"@primer/primitives": "7.6.0",
"@radix-ui/react-polymorphic": "0.0.14",
"@react-aria/ssr": "3.1.0",
"@styled-system/css": "5.1.5",
Expand Down
6 changes: 4 additions & 2 deletions src/Autocomplete/AutocompleteMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function getDefaultItemFilter<T extends AutocompleteMenuItem>(filterValue: strin
}
}

function getDefaultOnSelectionChange<T extends AutocompleteMenuItem>(
function getdefaultCheckedSelectionChange<T extends AutocompleteMenuItem>(
setInputValueFn: (value: string) => void
): OnSelectedChange<T> {
return function (itemOrItems) {
Expand Down Expand Up @@ -160,7 +160,9 @@ function AutocompleteMenu<T extends AutocompleteItemProps>(props: AutocompleteMe
const newSelectedItemIds = selectedItemIds.includes(item.id)
? otherSelectedItemIds
: [...otherSelectedItemIds, item.id]
const onSelectedChangeFn = onSelectedChange ? onSelectedChange : getDefaultOnSelectionChange(setInputValue)
const onSelectedChangeFn = onSelectedChange
? onSelectedChange
: getdefaultCheckedSelectionChange(setInputValue)

onSelectedChangeFn(
newSelectedItemIds.map(newSelectedItemId => getItemById(newSelectedItemId, items)) as T[]
Expand Down
Loading

0 comments on commit ae7650f

Please sign in to comment.