-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: APP-2700 - Switch component (#59)
Co-authored-by: Ruggero <[email protected]>
- Loading branch information
1 parent
4a3931f
commit ef99408
Showing
8 changed files
with
229 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { Switch, type ISwitchProps } from './switch'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { useState } from 'react'; | ||
import { Switch, type ISwitchProps } from './switch'; | ||
|
||
const meta: Meta<typeof Switch> = { | ||
title: 'components/Switch', | ||
component: Switch, | ||
tags: ['autodocs'], | ||
parameters: { | ||
design: { | ||
type: 'figma', | ||
url: 'https://www.figma.com/file/ISSDryshtEpB7SUSdNqAcw/branch/jfKRr1V9evJUp1uBeyP3Zz/Aragon-ODS?node-id=8850%3A12962&mode=dev', | ||
}, | ||
}, | ||
}; | ||
|
||
type Story = StoryObj<typeof Switch>; | ||
|
||
/** | ||
* `Switch` used as an uncontrolled component | ||
*/ | ||
export const Uncontrolled: Story = { | ||
args: { | ||
label: 'Show testnets', | ||
name: 'testnet', | ||
defaultChecked: true, | ||
onCheckedChanged: undefined, | ||
}, | ||
}; | ||
|
||
/** | ||
* Controlled usage of the `Switch` component | ||
*/ | ||
const ControlledComponent = (props: ISwitchProps) => { | ||
const [checked, setChecked] = useState(false); | ||
|
||
return <Switch checked={checked} onCheckedChanged={setChecked} {...props} />; | ||
}; | ||
|
||
export const Controlled: Story = { | ||
render: ({ onCheckedChanged, ...props }: ISwitchProps) => <ControlledComponent {...props} />, | ||
args: { | ||
label: 'Show testnets', | ||
name: 'testnet', | ||
}, | ||
}; | ||
|
||
export default meta; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
import { Switch, type ISwitchProps } from './switch'; | ||
|
||
describe('<Switch /> component', () => { | ||
const createTestComponent = (props?: Partial<ISwitchProps>) => { | ||
const completeProps: ISwitchProps = { ...props }; | ||
|
||
return <Switch {...completeProps} />; | ||
}; | ||
|
||
it('renders with default props', () => { | ||
render(createTestComponent()); | ||
expect(screen.getByRole('switch')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders with custom props', () => { | ||
const customProps = { checked: true, id: 'customId', disabled: true, label: 'customLabel', name: 'customName' }; | ||
|
||
render(createTestComponent(customProps)); | ||
|
||
const switchElement = screen.getByRole('switch'); | ||
expect(switchElement).toBeInTheDocument(); | ||
expect(switchElement).toHaveAttribute('data-state', 'checked'); | ||
expect(switchElement).toHaveAttribute('aria-checked', customProps.checked.toString()); | ||
expect(switchElement).toHaveAttribute('id', customProps.id); | ||
}); | ||
|
||
it('associates label correctly', () => { | ||
const label = 'customLabel'; | ||
render(createTestComponent({ label })); | ||
|
||
expect(screen.getByLabelText(label)).toBeInTheDocument(); | ||
}); | ||
|
||
it('generates unique ID when no ID is provided', () => { | ||
render(createTestComponent()); | ||
expect(screen.getByRole('switch')).toHaveAttribute('id'); | ||
}); | ||
|
||
it('invokes callback on state change and toggles state value', () => { | ||
const mockCallback = jest.fn(); | ||
render(createTestComponent({ checked: true, onCheckedChanged: mockCallback })); | ||
|
||
const switchElement = screen.getByRole('switch'); | ||
fireEvent.click(switchElement); | ||
|
||
expect(mockCallback).toHaveBeenCalledWith(false); | ||
}); | ||
|
||
it('renders as disabled when disabled prop is true', () => { | ||
const mockCallback = jest.fn(); | ||
render(createTestComponent({ disabled: true, onCheckedChanged: mockCallback })); | ||
|
||
const switchElement = screen.getByRole('switch'); | ||
fireEvent.click(switchElement); | ||
|
||
expect(switchElement).toBeDisabled(); | ||
expect(switchElement).toHaveClass('disabled:cursor-not-allowed'); | ||
expect(mockCallback).not.toHaveBeenCalled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import * as RadixSwitch from '@radix-ui/react-switch'; | ||
import classNames from 'classnames'; | ||
import { useId, type HtmlHTMLAttributes } from 'react'; | ||
|
||
const rootClassNames = classNames( | ||
'group peer w-10 cursor-default rounded-[40px] border border-neutral-200 bg-neutral-0 p-1', // Default | ||
'data-[state=checked]:border-primary-400 data-[state=checked]:shadow-primary-md', // State is checked | ||
'focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset', // Focus | ||
'disabled:cursor-not-allowed disabled:bg-neutral-100 disabled:data-[state=checked]:border-neutral-200 disabled:data-[state=checked]:shadow-none', // Disabled | ||
); | ||
|
||
const thumbClassNames = classNames( | ||
'block h-4 w-4 rounded-full bg-neutral-300 transition-transform duration-100 will-change-transform', // Default | ||
'data-[state=checked]:translate-x-[14px] data-[state=checked]:bg-primary-400', // State is checked | ||
'group-disabled:bg-neutral-200 group-disabled:data-[state=checked]:bg-neutral-300', // Disabled | ||
); | ||
|
||
// using `peer` since the parent div is not focusable nor able to be disabled | ||
const labelClassNames = classNames( | ||
'text-sm/tight font-semibold text-neutral-600', // Default | ||
'peer-disabled:text-neutral-300 peer-disabled:peer-data-[state=checked]:text-neutral-600', // Disabled | ||
); | ||
|
||
export interface ISwitchProps extends HtmlHTMLAttributes<HTMLDivElement> { | ||
/** | ||
* Indicates whether the switch is checked | ||
*/ | ||
checked?: boolean; | ||
/** | ||
* CSS class name | ||
*/ | ||
className?: string; | ||
/** | ||
* The default checked state of the switch | ||
* @default false | ||
*/ | ||
defaultChecked?: boolean; | ||
/** | ||
* Indicates whether the switch is disabled | ||
* @default false | ||
*/ | ||
disabled?: boolean; | ||
/** | ||
* The ID of the switch | ||
*/ | ||
id?: string; | ||
/** | ||
* The label of the switch | ||
*/ | ||
label?: string; | ||
/** | ||
* The name of the switch | ||
*/ | ||
name?: string; | ||
/** | ||
* Event handler for when the checked state changes | ||
* @param checked - The new checked state | ||
*/ | ||
onCheckedChanged?: (checked: boolean) => void; | ||
} | ||
|
||
/** | ||
* Switch component | ||
*/ | ||
export const Switch: React.FC<ISwitchProps> = (props) => { | ||
const { | ||
checked, | ||
className, | ||
defaultChecked = false, | ||
disabled = false, | ||
id: propId, | ||
label, | ||
name, | ||
onCheckedChanged, | ||
...otherProps | ||
} = props; | ||
|
||
// use randomly generated id when non provided | ||
const internalId = useId(); | ||
const id = propId ?? internalId; | ||
|
||
const switchProps = { | ||
id, | ||
name, | ||
checked, | ||
disabled, | ||
defaultChecked, | ||
}; | ||
|
||
return ( | ||
<div className={classNames('inline-flex items-center gap-x-2 md:gap-x-3', className)} {...otherProps}> | ||
<RadixSwitch.Root {...switchProps} className={rootClassNames} onCheckedChange={onCheckedChanged}> | ||
<RadixSwitch.Thumb className={thumbClassNames} /> | ||
</RadixSwitch.Root> | ||
{label && ( | ||
<label htmlFor={id} className={labelClassNames}> | ||
<span>{label}</span> | ||
</label> | ||
)} | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1872,6 +1872,20 @@ | |
"@babel/runtime" "^7.13.10" | ||
"@radix-ui/react-compose-refs" "1.0.1" | ||
|
||
"@radix-ui/react-switch@^1.0.0": | ||
version "1.0.3" | ||
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e" | ||
integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow== | ||
dependencies: | ||
"@babel/runtime" "^7.13.10" | ||
"@radix-ui/primitive" "1.0.1" | ||
"@radix-ui/react-compose-refs" "1.0.1" | ||
"@radix-ui/react-context" "1.0.1" | ||
"@radix-ui/react-primitive" "1.0.3" | ||
"@radix-ui/react-use-controllable-state" "1.0.1" | ||
"@radix-ui/react-use-previous" "1.0.1" | ||
"@radix-ui/react-use-size" "1.0.1" | ||
|
||
"@radix-ui/[email protected]": | ||
version "1.0.4" | ||
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz#f5b5c8c477831b013bec3580c55e20a68179d6ec" | ||
|