Skip to content

Commit 76c721a

Browse files
feat: Add spinner to Toggle component (#3803)
1 parent 535feae commit 76c721a

File tree

3 files changed

+130
-9
lines changed

3 files changed

+130
-9
lines changed

src/ui/Toggle/Toggle.stories.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,25 @@ export const DisabledToggle: Story = {
4646
)
4747
},
4848
}
49+
50+
export const LoadingToggle: Story = {
51+
render: (args) => {
52+
const [toggle, setToggle] = useState(false)
53+
const [isLoading, setIsLoading] = useState(false)
54+
const toggler = async () => {
55+
setIsLoading(true)
56+
setTimeout(() => {
57+
setToggle(!toggle)
58+
setIsLoading(false)
59+
}, 2000)
60+
}
61+
return (
62+
<Toggle
63+
value={toggle}
64+
isLoading={isLoading}
65+
{...args}
66+
onClick={toggler}
67+
/>
68+
)
69+
},
70+
}

src/ui/Toggle/Toggle.test.tsx

+84
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,88 @@ describe('Toggle', () => {
180180
expect(button).toHaveAttribute('disabled')
181181
})
182182
})
183+
184+
describe('isLoading behavior', () => {
185+
describe('when isLoading is true', () => {
186+
it('renders spinner', () => {
187+
render(
188+
<Toggle
189+
label="🐕"
190+
dataMarketing="marketing"
191+
value={true}
192+
disabled={false}
193+
isLoading={true}
194+
onClick={() => {}}
195+
/>
196+
)
197+
198+
const spinner = screen.getByTestId('toggle-loading-spinner')
199+
expect(spinner).toBeInTheDocument()
200+
})
201+
202+
describe('and is clicked', () => {
203+
it('does not fire onClick', async () => {
204+
const { user } = setup()
205+
const mockFn = vi.fn()
206+
render(
207+
<Toggle
208+
label="🐕"
209+
dataMarketing="marketing"
210+
value={false}
211+
disabled={false}
212+
onClick={mockFn}
213+
isLoading={true}
214+
/>
215+
)
216+
217+
const button = screen.getByRole('button')
218+
219+
await user.click(button)
220+
221+
expect(mockFn).not.toHaveBeenCalled()
222+
})
223+
})
224+
})
225+
226+
describe('when isLoading is false', () => {
227+
it('does not render spinner', () => {
228+
render(
229+
<Toggle
230+
label="🐕"
231+
dataMarketing="marketing"
232+
value={true}
233+
disabled={false}
234+
isLoading={false}
235+
onClick={() => {}}
236+
/>
237+
)
238+
239+
const spinner = screen.queryByTestId('toggle-loading-spinner')
240+
expect(spinner).not.toBeInTheDocument()
241+
})
242+
243+
describe('and is clicked', () => {
244+
it('does fire onClick', async () => {
245+
const { user } = setup()
246+
const mockFn = vi.fn()
247+
render(
248+
<Toggle
249+
label="🐕"
250+
dataMarketing="marketing"
251+
value={false}
252+
disabled={false}
253+
onClick={mockFn}
254+
isLoading={false}
255+
/>
256+
)
257+
258+
const button = screen.getByRole('button')
259+
260+
await user.click(button)
261+
262+
expect(mockFn).toHaveBeenCalled()
263+
})
264+
})
265+
})
266+
})
183267
})

src/ui/Toggle/Toggle.tsx

+24-9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface ToggleProps {
88
label: string
99
onClick: () => void
1010
disabled?: boolean
11+
isLoading?: boolean
1112
dataMarketing: string
1213
}
1314

@@ -16,6 +17,7 @@ function Toggle({
1617
value = false,
1718
onClick,
1819
disabled = false,
20+
isLoading = false,
1921
dataMarketing,
2022
}: ToggleProps) {
2123
const ID = uniqueId('toggle')
@@ -24,7 +26,7 @@ function Toggle({
2426
<div
2527
data-marketing={`${ID}-${dataMarketing}`}
2628
onClick={() => {
27-
if (!disabled) {
29+
if (!disabled && !isLoading) {
2830
onClick()
2931
}
3032
}}
@@ -43,7 +45,7 @@ function Toggle({
4345
'bg-toggle-active': value,
4446
'bg-toggle-inactive': !value && !disabled,
4547
'bg-toggle-disabled': disabled,
46-
'cursor-not-allowed': disabled,
48+
'cursor-not-allowed': disabled || isLoading,
4749
}
4850
)}
4951
aria-pressed="false"
@@ -63,18 +65,31 @@ function Toggle({
6365
)}
6466
>
6567
<div
66-
className={cn({
68+
className={cn('flex size-5 items-center justify-center', {
6769
'text-toggle-active': value,
6870
'text-toggle-inactive': !value && !disabled,
6971
'text-toggle-disabled': disabled,
7072
})}
7173
>
72-
<Icon
73-
name={value ? 'check' : 'x'}
74-
label={value ? 'check' : 'x'}
75-
variant="solid"
76-
size="flex"
77-
/>
74+
{isLoading ? (
75+
<div
76+
data-testid="toggle-loading-spinner"
77+
className={cn(
78+
'size-4 animate-spin rounded-full border-4 border-white',
79+
{
80+
'border-t-toggle-active': value,
81+
'border-t-toggle-inactive': !value && !disabled,
82+
}
83+
)}
84+
/>
85+
) : (
86+
<Icon
87+
name={value ? 'check' : 'x'}
88+
label={value ? 'check' : 'x'}
89+
variant="solid"
90+
size="flex"
91+
/>
92+
)}
7893
</div>
7994
</span>
8095
</button>

0 commit comments

Comments
 (0)