Skip to content

Commit ae7650f

Browse files
mperrotticolebemissimurai
authored
Add ToggleSwitch component (#1933)
* 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>
1 parent adbcd3b commit ae7650f

File tree

15 files changed

+1342
-47
lines changed

15 files changed

+1342
-47
lines changed

.changeset/stupid-carrots-jam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': minor
3+
---
4+
5+
Adds a toggle switch component

docs/content/ToggleSwitch.mdx

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
---
2+
componentId: toggle_switch
3+
title: ToggleSwitch
4+
description: Toggles a setting on or off, and immediately saves the change
5+
status: Alpha
6+
source: https://github.com/primer/react/blob/main/src/ToggleSwitch.tsx
7+
storybook: '/react/storybook?path=/story/toggleswitch-examples--default'
8+
---
9+
10+
## Examples
11+
12+
### Basic
13+
14+
```jsx live
15+
<Box display="flex" maxWidth="300px">
16+
<Box flexGrow={1} fontSize={2} fontWeight="bold" id="switchLabel">
17+
Notifications
18+
</Box>
19+
<ToggleSwitch aria-labelledby="switchLabel" />
20+
</Box>
21+
```
22+
23+
### Uncontrolled with default value
24+
25+
```jsx live
26+
<Box display="flex" maxWidth="300px">
27+
<Box flexGrow={1} fontSize={2} fontWeight="bold" id="switchLabel">
28+
Notifications
29+
</Box>
30+
<ToggleSwitch defaultChecked aria-labelledby="switchLabel" />
31+
</Box>
32+
```
33+
34+
### Controlled
35+
36+
```javascript noinline live
37+
const Controlled = () => {
38+
const [isOn, setIsOn] = React.useState(false)
39+
40+
const onClick = () => {
41+
setIsOn(!isOn)
42+
}
43+
44+
const handleSwitchChange = on => {
45+
console.log(`new switch "on" state: ${on}`)
46+
}
47+
48+
return (
49+
<>
50+
<Box display="flex" maxWidth="300px">
51+
<Box flexGrow={1} fontSize={2} fontWeight="bold" id="switchLabel">
52+
Notifications
53+
</Box>
54+
<ToggleSwitch onClick={onClick} onChange={handleSwitchChange} checked={isOn} aria-labelledby="switchLabel" />
55+
</Box>
56+
<p>The switch is {isOn ? 'on' : 'off'}</p>
57+
</>
58+
)
59+
}
60+
61+
render(Controlled)
62+
```
63+
64+
### Small
65+
66+
```jsx live
67+
<Box display="flex" maxWidth="300px">
68+
<Box flexGrow={1} fontSize={1} fontWeight="bold" id="switchLabel">
69+
Notifications
70+
</Box>
71+
<ToggleSwitch aria-labelledby="switchLabel" size="small" />
72+
</Box>
73+
```
74+
75+
### Delayed toggle with loading state
76+
77+
```javascript noinline live
78+
const LoadingToggle = () => {
79+
const [loading, setLoading] = React.useState(false)
80+
const [isOn, setIsOn] = React.useState(false)
81+
82+
async function switchSlowly(currentOn) {
83+
await new Promise(resolve => setTimeout(resolve, 1500))
84+
return await !currentOn
85+
}
86+
87+
async function onClick() {
88+
setLoading(!loading)
89+
const newSwitchState = await switchSlowly(isOn)
90+
setIsOn(newSwitchState)
91+
}
92+
93+
const handleSwitchChange = React.useCallback(
94+
on => {
95+
setLoading(false)
96+
},
97+
[setLoading]
98+
)
99+
100+
return (
101+
<>
102+
<Box display="flex" maxWidth="300px">
103+
<Box flexGrow={1} fontSize={2} fontWeight="bold" id="switchLabel">
104+
Notifications
105+
</Box>
106+
<ToggleSwitch
107+
aria-labelledby="switchLabel"
108+
loading={loading}
109+
checked={isOn}
110+
onClick={onClick}
111+
onChange={handleSwitchChange}
112+
/>
113+
</Box>
114+
<p>The switch is {isOn ? 'on' : 'off'}</p>
115+
</>
116+
)
117+
}
118+
119+
render(LoadingToggle)
120+
```
121+
122+
### Disabled
123+
124+
```jsx live
125+
<Box display="flex" maxWidth="300px">
126+
<Box flexGrow={1} fontSize={2} fontWeight="bold" id="switchLabel">
127+
Notifications
128+
</Box>
129+
<ToggleSwitch aria-labelledby="switchLabel" disabled />
130+
</Box>
131+
```
132+
133+
### With associated caption text
134+
135+
```jsx live
136+
<Box display="flex">
137+
<Box flexGrow={1}>
138+
<Text fontSize={2} fontWeight="bold" id="switchLabel" display="block">
139+
Notifications
140+
</Text>
141+
<Text color="fg.subtle" fontSize={1} id="switchCaption" display="block">
142+
Notifications will be delivered via email and the GitHub notification center
143+
</Text>
144+
</Box>
145+
<ToggleSwitch aria-labelledby="switchLabel" aria-describedby="switchCaption" />
146+
</Box>
147+
```
148+
149+
### Left-aligned with label
150+
151+
```jsx live
152+
<>
153+
<Text fontSize={2} fontWeight="bold" id="switchLabel" display="block" mb={1}>
154+
Notifications
155+
</Text>
156+
<ToggleSwitch statusLabelPosition="end" aria-labelledby="switchLabel" />
157+
</>
158+
```
159+
160+
## Props
161+
162+
<PropsTable>
163+
<PropsTableRow name="aria-describedby" type="string" description="The id of the DOM node that describes the switch" />
164+
<PropsTableRow
165+
name="aria-labelledby"
166+
type="string"
167+
required
168+
description="The id of the DOM node that labels the switch"
169+
/>
170+
<PropsTableRow name="defaultChecked" type="boolean" description="Uncontrolled - whether the switch is turned on" />
171+
<PropsTableRow name="disabled" type="boolean" description="Whether the switch is ready for user input" />
172+
<PropsTableRow name="loading" type="boolean" description="Whether the switch's value is being calculated" />
173+
<PropsTableRow name="checked" type="boolean" description="Whether the switch is turned on" />
174+
<PropsTableRow
175+
name="onChange"
176+
type="(on: boolean) => void"
177+
description="The callback that is called when the switch is toggled on or off"
178+
/>
179+
<PropsTableRow
180+
name="onClick"
181+
type="(e: MouseEvent) => void"
182+
description="The callback that is called when the switch is clicked"
183+
/>
184+
<PropsTableRow name="size" type="'small' | 'medium'" defaultValue="'medium'" description="Size of the switch" />
185+
<PropsTableRow
186+
name="statusLabelPosition"
187+
type="'start' | 'end'"
188+
defaultValue="'start'"
189+
description={
190+
<>
191+
<div>Whether the "on" and "off" labels should appear before or after the switch.</div>
192+
<div>
193+
<Text fontWeight="bold">This should only be changed when the switch's alignment needs to be adjusted.</Text>{' '}
194+
For example: It needs to be left-aligned because the label appears above it and the caption appears below it.
195+
</div>
196+
</>
197+
}
198+
/>
199+
</PropsTable>
200+
201+
## Status
202+
203+
<ComponentChecklist
204+
items={{
205+
propsDocumented: true,
206+
noUnnecessaryDeps: true,
207+
adaptsToThemes: true,
208+
adaptsToScreenSizes: true,
209+
fullTestCoverage: true,
210+
usedInProduction: false,
211+
usageExamplesDocumented: true,
212+
hasStorybookStories: true,
213+
designReviewed: false,
214+
a11yReviewed: false,
215+
stableApi: false,
216+
addressedApiFeedback: false,
217+
hasDesignGuidelines: false,
218+
hasFigmaComponent: false
219+
}}
220+
/>

docs/src/@primer/gatsby-theme-doctocat/nav.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@
123123
url: /StyledOcticon
124124
- title: SubNav
125125
url: /SubNav
126+
- title: ToggleSwitch
127+
url: /ToggleSwitch
126128
- title: TabNav
127129
url: /TabNav
128130
- title: Textarea

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
"dependencies": {
7878
"@primer/behaviors": "1.1.0",
7979
"@primer/octicons-react": "16.1.1",
80-
"@primer/primitives": "7.5.1",
80+
"@primer/primitives": "7.6.0",
8181
"@radix-ui/react-polymorphic": "0.0.14",
8282
"@react-aria/ssr": "3.1.0",
8383
"@styled-system/css": "5.1.5",

src/Autocomplete/AutocompleteMenu.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function getDefaultItemFilter<T extends AutocompleteMenuItem>(filterValue: strin
2323
}
2424
}
2525

26-
function getDefaultOnSelectionChange<T extends AutocompleteMenuItem>(
26+
function getdefaultCheckedSelectionChange<T extends AutocompleteMenuItem>(
2727
setInputValueFn: (value: string) => void
2828
): OnSelectedChange<T> {
2929
return function (itemOrItems) {
@@ -160,7 +160,9 @@ function AutocompleteMenu<T extends AutocompleteItemProps>(props: AutocompleteMe
160160
const newSelectedItemIds = selectedItemIds.includes(item.id)
161161
? otherSelectedItemIds
162162
: [...otherSelectedItemIds, item.id]
163-
const onSelectedChangeFn = onSelectedChange ? onSelectedChange : getDefaultOnSelectionChange(setInputValue)
163+
const onSelectedChangeFn = onSelectedChange
164+
? onSelectedChange
165+
: getdefaultCheckedSelectionChange(setInputValue)
164166

165167
onSelectedChangeFn(
166168
newSelectedItemIds.map(newSelectedItemId => getItemById(newSelectedItemId, items)) as T[]

0 commit comments

Comments
 (0)