-
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.
feat: APP-2694 - Implement Toggle and ToggleGroups components (#58)
- Loading branch information
Showing
18 changed files
with
366 additions
and
3 deletions.
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
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,2 @@ | ||
export * from './toggle'; | ||
export * from './toggleGroup'; |
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 { Toggle, type IToggleProps } from './toggle'; |
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,71 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { useState } from 'react'; | ||
import { ToggleGroup } from '../toggleGroup'; | ||
import { Toggle, type IToggleProps } from './toggle'; | ||
|
||
const meta: Meta<typeof Toggle> = { | ||
title: 'components/Toggles/Toggle', | ||
component: Toggle, | ||
tags: ['autodocs'], | ||
parameters: { | ||
design: { | ||
type: 'figma', | ||
url: 'https://www.figma.com/file/jfKRr1V9evJUp1uBeyP3Zz/v1.0.0?type=design&node-id=9778-14&mode=dev', | ||
}, | ||
}, | ||
}; | ||
|
||
type Story = StoryObj<typeof Toggle>; | ||
|
||
/** | ||
* Default usage example of the Toggle component. | ||
*/ | ||
export const Default: Story = { | ||
render: (props) => ( | ||
<ToggleGroup isMultiSelect={false}> | ||
<Toggle {...props} /> | ||
</ToggleGroup> | ||
), | ||
args: { | ||
value: 'value', | ||
label: 'Label', | ||
}, | ||
}; | ||
|
||
const ControllerComponent = (props: IToggleProps) => { | ||
const [value, setValue] = useState<string>(); | ||
|
||
return ( | ||
<ToggleGroup isMultiSelect={false} value={value} onChange={setValue}> | ||
<Toggle {...props} /> | ||
</ToggleGroup> | ||
); | ||
}; | ||
|
||
/** | ||
* Controlled usage example of the Toggle component. | ||
*/ | ||
export const Controlled: Story = { | ||
render: (props) => <ControllerComponent {...props} />, | ||
args: { | ||
value: 'value', | ||
label: 'Label', | ||
}, | ||
}; | ||
|
||
/** | ||
* Disabled Toggle component. | ||
*/ | ||
export const Disabled: Story = { | ||
render: (props) => ( | ||
<ToggleGroup isMultiSelect={false}> | ||
<Toggle {...props} /> | ||
</ToggleGroup> | ||
), | ||
args: { | ||
disabled: true, | ||
label: 'Disabled', | ||
}, | ||
}; | ||
|
||
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,37 @@ | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
import { ToggleGroup } from '../toggleGroup'; | ||
import { Toggle, type IToggleProps } from './toggle'; | ||
|
||
describe('<Toggle /> component', () => { | ||
const createTestComponent = (props?: Partial<IToggleProps>) => { | ||
const completeProps: IToggleProps = { | ||
label: 'label', | ||
value: 'value', | ||
...props, | ||
}; | ||
|
||
return ( | ||
<ToggleGroup isMultiSelect={false}> | ||
<Toggle {...completeProps} /> | ||
</ToggleGroup> | ||
); | ||
}; | ||
|
||
it('renders a toggle with the specified label', () => { | ||
const label = 'Toggle Label'; | ||
render(createTestComponent({ label })); | ||
expect(screen.getByRole('radio', { name: label })).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the toggle as disabled when the disabled prop is set to true', () => { | ||
const disabled = true; | ||
render(createTestComponent({ disabled })); | ||
expect(screen.getByRole('radio')).toBeDisabled(); | ||
}); | ||
|
||
it('renders the toggle as active when clicked', () => { | ||
render(createTestComponent()); | ||
fireEvent.click(screen.getByRole('radio')); | ||
expect(screen.getByRole('radio').className).toContain('text-neutral-800'); | ||
}); | ||
}); |
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,39 @@ | ||
import { ToggleGroupItem as RadixToggle } from '@radix-ui/react-toggle-group'; | ||
import classNames from 'classnames'; | ||
import type { ComponentProps } from 'react'; | ||
|
||
export interface IToggleProps extends Omit<ComponentProps<'button'>, 'ref'> { | ||
/** | ||
* Value of the toggle. | ||
*/ | ||
value: string; | ||
/** | ||
* Label of the toggle. | ||
*/ | ||
label: string; | ||
} | ||
|
||
/** | ||
* The Toggle component is a button that handles the "on" and "off" states. | ||
* | ||
* **NOTE**: The component must be used inside a `<ToggleGroup />` component in order to work properly. | ||
*/ | ||
export const Toggle: React.FC<IToggleProps> = (props) => { | ||
const { className, label, value, disabled, ...otherProps } = props; | ||
|
||
const toggleClasses = classNames( | ||
'flex h-10 items-center rounded-[40px] border border-neutral-100 px-4', // Default | ||
'focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset', // Focus state | ||
'hover:enabled:border-neutral-200 hover:enabled:shadow-primary-md', // Hover state | ||
'data-[state=off]:enabled:bg-neutral-0 data-[state=off]:enabled:text-neutral-600', // Default state | ||
'data-[state=on]:enabled:bg-neutral-100 data-[state=on]:enabled:text-neutral-800', // Active state | ||
'disabled:bg-neutral-100 disabled:text-neutral-300', // Disabled state | ||
className, | ||
); | ||
|
||
return ( | ||
<RadixToggle className={toggleClasses} disabled={disabled} value={value} {...otherProps}> | ||
<p className="text-sm font-semibold leading-normal md:text-base">{label}</p> | ||
</RadixToggle> | ||
); | ||
}; |
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 { ToggleGroup, type IToggleGroupProps } from './toggleGroup'; |
72 changes: 72 additions & 0 deletions
72
src/components/toggles/toggleGroup/toggleGroup.stories.tsx
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,72 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { useState } from 'react'; | ||
import { Toggle } from '../toggle'; | ||
import { ToggleGroup, type IToggleGroupProps } from './toggleGroup'; | ||
|
||
const meta: Meta<typeof ToggleGroup> = { | ||
title: 'components/Toggles/ToggleGroup', | ||
component: ToggleGroup, | ||
tags: ['autodocs'], | ||
parameters: { | ||
design: { | ||
type: 'figma', | ||
url: 'https://www.figma.com/file/jfKRr1V9evJUp1uBeyP3Zz/v1.0.0?type=design&node-id=11857-23553&mode=dev', | ||
}, | ||
}, | ||
}; | ||
|
||
type Story = StoryObj<typeof ToggleGroup>; | ||
|
||
/** | ||
* Default usage example of the ToggleGroup component. | ||
*/ | ||
export const Default: Story = { | ||
render: (props) => ( | ||
<ToggleGroup {...props}> | ||
<Toggle value="multisig" label="Multisig" /> | ||
<Toggle value="token-based" label="Token Based" /> | ||
</ToggleGroup> | ||
), | ||
}; | ||
|
||
const ControlledComponent = (props: Omit<IToggleGroupProps, 'value' | 'onChange' | 'isMultiSelect'>) => { | ||
const [value, setValue] = useState<string>(); | ||
|
||
return ( | ||
<ToggleGroup isMultiSelect={false} value={value} onChange={setValue} {...props}> | ||
<Toggle value="ethereum" label="Ethereum" /> | ||
<Toggle value="polygon" label="Polygon" /> | ||
<Toggle value="base" label="Base" /> | ||
<Toggle value="arbitrum" label="Arbitrum" /> | ||
<Toggle value="bsc" label="Binance Smart Chain" /> | ||
</ToggleGroup> | ||
); | ||
}; | ||
|
||
/** | ||
* Controlled usage example of the ToggleGroup component. | ||
*/ | ||
export const Controlled: Story = { | ||
render: ({ value, onChange, isMultiSelect, ...props }) => <ControlledComponent {...props} />, | ||
}; | ||
|
||
const MultiSelectComponent = (props: Omit<IToggleGroupProps, 'value' | 'onChange' | 'isMultiSelect'>) => { | ||
const [value, setValue] = useState<string[]>(); | ||
|
||
return ( | ||
<ToggleGroup isMultiSelect={true} value={value} onChange={setValue} {...props}> | ||
<Toggle value="all" label="All DAOs" /> | ||
<Toggle value="member" label="Member" /> | ||
<Toggle value="following" label="Following" disabled={true} /> | ||
</ToggleGroup> | ||
); | ||
}; | ||
|
||
/** | ||
* ToggleGroup component used with multiple selection. | ||
*/ | ||
export const MultiSelect: Story = { | ||
render: ({ value, onChange, isMultiSelect, ...props }) => <MultiSelectComponent {...props} />, | ||
}; | ||
|
||
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,60 @@ | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
import { Toggle } from '../toggle'; | ||
import { ToggleGroup, type IToggleGroupBaseProps, type IToggleGroupProps } from './toggleGroup'; | ||
|
||
describe('<ToggleGroup /> component', () => { | ||
const createTestComponent = (props: Partial<IToggleGroupProps> = {}) => { | ||
if (props?.isMultiSelect) { | ||
return <ToggleGroup isMultiSelect={true} {...props} />; | ||
} | ||
|
||
const { isMultiSelect, ...otherProps } = props as IToggleGroupBaseProps<false>; | ||
|
||
return <ToggleGroup isMultiSelect={false} {...otherProps} />; | ||
}; | ||
|
||
it('renders the children components', () => { | ||
const children = [ | ||
<Toggle key="first" value="first" label="First" />, | ||
<Toggle key="second" value="second" label="Second" />, | ||
]; | ||
render(createTestComponent({ children })); | ||
expect(screen.getAllByRole('radio')).toHaveLength(children.length); | ||
}); | ||
|
||
it('correctly updates the active value on toggle click', () => { | ||
const onChange = jest.fn(); | ||
const value = 'test'; | ||
const children = [<Toggle key={value} value={value} label={value} />]; | ||
const { rerender } = render(createTestComponent({ onChange, children })); | ||
|
||
fireEvent.click(screen.getByRole('radio')); | ||
expect(onChange).toHaveBeenCalledWith(value); | ||
|
||
rerender(createTestComponent({ value, onChange, children })); | ||
|
||
fireEvent.click(screen.getByRole('radio')); | ||
expect(onChange).toHaveBeenCalledWith(''); | ||
}); | ||
|
||
it('correctly updates the active values on toggle click on multi-select variant', () => { | ||
const onChange = jest.fn(); | ||
const isMultiSelect = true; | ||
const firstValue = 'first'; | ||
const secondValue = 'second'; | ||
const children = [ | ||
<Toggle key={firstValue} value={firstValue} label={firstValue} />, | ||
<Toggle key={secondValue} value={secondValue} label={secondValue} />, | ||
]; | ||
const { rerender } = render(createTestComponent({ onChange, children, isMultiSelect })); | ||
|
||
fireEvent.click(screen.getByRole('button', { name: firstValue })); | ||
const newValue = [firstValue]; | ||
expect(onChange).toHaveBeenCalledWith(newValue); | ||
|
||
rerender(createTestComponent({ value: newValue, onChange, children, isMultiSelect })); | ||
|
||
fireEvent.click(screen.getByRole('button', { name: secondValue })); | ||
expect(onChange).toHaveBeenCalledWith([...newValue, secondValue]); | ||
}); | ||
}); |
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,44 @@ | ||
import { ToggleGroup as RadixToggleGroup } from '@radix-ui/react-toggle-group'; | ||
import classNames from 'classnames'; | ||
import type { ComponentProps } from 'react'; | ||
|
||
export type ToggleGroupValue<TMulti extends boolean> = TMulti extends true ? string[] | undefined : string | undefined; | ||
|
||
export interface IToggleGroupBaseProps<TMulti extends boolean> | ||
extends Omit<ComponentProps<'div'>, 'value' | 'onChange' | 'defaultValue' | 'ref' | 'dir'> { | ||
/** | ||
* Allows multiple toggles to be selected at the same time when set to true. | ||
*/ | ||
isMultiSelect: TMulti; | ||
/** | ||
* Current value of the toggle selection. | ||
*/ | ||
value?: ToggleGroupValue<TMulti>; | ||
/** | ||
* Callback called on toggle selection change. | ||
*/ | ||
onChange?: (value: ToggleGroupValue<TMulti>) => void; | ||
} | ||
|
||
export type IToggleGroupProps = IToggleGroupBaseProps<true> | IToggleGroupBaseProps<false>; | ||
|
||
export const ToggleGroup = (props: IToggleGroupProps) => { | ||
const { value, onChange, isMultiSelect, className, ...otherProps } = props; | ||
const classes = classNames('flex flex-row flex-wrap gap-2 md:gap-3', className); | ||
|
||
if (isMultiSelect === true) { | ||
return ( | ||
<RadixToggleGroup | ||
type="multiple" | ||
className={classes} | ||
value={value} | ||
onValueChange={onChange} | ||
{...otherProps} | ||
/> | ||
); | ||
} | ||
|
||
return ( | ||
<RadixToggleGroup type="single" className={classes} value={value} onValueChange={onChange} {...otherProps} /> | ||
); | ||
}; |
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 * from './utils'; |
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 { testLogger } from './testLogger'; |
Oops, something went wrong.