-
Notifications
You must be signed in to change notification settings - Fork 12
feat(Toggle): add new Toggle, ToggleGroup component
#628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
ae62a5f
add new `ToggleGroup`
noahchoii 4131270
add new `Toggle`
noahchoii 2b38489
remove aria-orientation from ToggleGroup.Root
noahchoii 397edff
add toggle, toggle-group test cases
noahchoii 5ac593b
add changesets
noahchoii 016df5e
add snapshots
noahchoii 556211f
add missing storybook args
noahchoii 7ff23f5
remove unused pressed args
noahchoii c963a8d
apply icon size variation
noahchoii 5b82a09
apply semantic colors
noahchoii c2f21cf
update snapshots
noahchoii 0e1587f
modify toggle interaction scale
noahchoii 7c6ee67
add enter pressed test case
noahchoii 243c796
Merge branch 'main' into feature/vapor-185
noahchoii da03ad3
remove toggle-group border
noahchoii 0ed433b
remove variant prop from toggle-group
noahchoii 41fba50
update snapshots
noahchoii c531453
remove unused variant context
noahchoii c420249
remove separator
noahchoii 27f3a40
update snapshots
noahchoii 2f216a4
rename togglegroup
noahchoii 05a70d5
rename toggle group variants type
noahchoii ff5495b
add pressed to logical-states
noahchoii ec12de5
lint
noahchoii 9bd0750
Merge branch 'main' into feature/vapor-185
noahchoii 02b241c
remove unnecessary index.parts
noahchoii f6cbf04
remove .Root
noahchoii fb08c88
replace clsx with cn
noahchoii File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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,5 @@ | ||
| --- | ||
| '@vapor-ui/core': minor | ||
| --- | ||
|
|
||
| add new `ToggleGroup` component |
This file contains hidden or 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,5 @@ | ||
| --- | ||
| '@vapor-ui/core': minor | ||
| --- | ||
|
|
||
| add new `Toggle` component |
Binary file added
BIN
+16 KB
packages/core/__tests__/screenshots/toggle--test-bed-1-chrome-darwin-.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+16 KB
packages/core/__tests__/screenshots/toggle--test-bed-1-edge-darwin-.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+33.6 KB
packages/core/__tests__/screenshots/toggle--test-bed-1-firefox-darwin-.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+32.3 KB
packages/core/__tests__/screenshots/toggle--test-bed-1-safari-darwin-.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+12.6 KB
packages/core/__tests__/screenshots/togglegroup--test-bed-1-chrome-darwin-.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+12.6 KB
packages/core/__tests__/screenshots/togglegroup--test-bed-1-edge-darwin-.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+29.1 KB
packages/core/__tests__/screenshots/togglegroup--test-bed-1-firefox-darwin-.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+27.5 KB
packages/core/__tests__/screenshots/togglegroup--test-bed-1-safari-darwin-.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or 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 hidden or 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 './toggle-group'; |
19 changes: 19 additions & 0 deletions
19
packages/core/src/components/toggle-group/toggle-group.css.ts
This file contains hidden or 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,19 @@ | ||
| import type { RecipeVariants } from '@vanilla-extract/recipes'; | ||
|
|
||
| import { componentRecipe } from '~/styles/mixins/layer-style.css'; | ||
| import { vars } from '~/styles/themes.css'; | ||
|
|
||
| export const root = componentRecipe({ | ||
| base: { | ||
| display: 'inline-flex', | ||
| alignItems: 'center', | ||
| gap: vars.size.space['050'], | ||
| }, | ||
|
|
||
| defaultVariants: { size: 'md' }, | ||
| variants: { | ||
| size: { sm: {}, md: {}, lg: {}, xl: {} }, | ||
| }, | ||
| }); | ||
|
|
||
| export type RootVariants = NonNullable<RecipeVariants<typeof root>>; | ||
115 changes: 115 additions & 0 deletions
115
packages/core/src/components/toggle-group/toggle-group.stories.tsx
This file contains hidden or 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,115 @@ | ||
| import type { Meta, StoryObj } from '@storybook/react-vite'; | ||
| import { HeartIcon } from '@vapor-ui/icons'; | ||
|
|
||
| import { ToggleGroup } from '.'; | ||
| import { Grid } from '../grid'; | ||
| import { Toggle } from '../toggle'; | ||
| import { VStack } from '../v-stack'; | ||
|
|
||
| export default { | ||
| title: 'ToggleGroup', | ||
| component: ToggleGroup, | ||
| argTypes: { | ||
| size: { | ||
| control: { type: 'inline-radio' }, | ||
| options: ['sm', 'md', 'lg', 'xl'], | ||
| }, | ||
| disabled: { | ||
| control: { type: 'boolean' }, | ||
| }, | ||
| }, | ||
| } satisfies Meta<typeof ToggleGroup>; | ||
|
|
||
| type Story = StoryObj<typeof ToggleGroup>; | ||
|
|
||
| export const Default: Story = { | ||
| render: (args) => { | ||
| return ( | ||
| <ToggleGroup {...args}> | ||
| <Toggle value="item1"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| <Toggle value="item2"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| <Toggle value="item3"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
|
|
||
| <Toggle value="item4"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| <Toggle value="item5"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| <Toggle value="item6"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| </ToggleGroup> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const TestBed: Story = { | ||
| render: (args) => { | ||
| return ( | ||
| <Grid.Root templateColumns="repeat(2, 1fr)" templateRows="auto" $css={{ gap: '$100' }}> | ||
| <Grid.Item> | ||
| <VStack $css={{ gap: '$100', alignItems: 'flex-start' }}> | ||
| {renderCases({ size: 'sm' })} | ||
| {renderCases({ size: 'md' })} | ||
| {renderCases({ size: 'lg' })} | ||
| {renderCases({ size: 'xl' })} | ||
| </VStack> | ||
| </Grid.Item> | ||
|
|
||
| <Grid.Item> | ||
| <VStack $css={{ gap: '$100', alignItems: 'flex-start' }}> | ||
| {renderCases({ size: 'sm', disabled: true })} | ||
| {renderCases({ size: 'md', disabled: true })} | ||
| {renderCases({ size: 'lg', disabled: true })} | ||
| {renderCases({ size: 'xl', disabled: true })} | ||
| </VStack> | ||
| </Grid.Item> | ||
|
|
||
| <ToggleGroup {...args} value={['item3', 'item4']}> | ||
| <Toggle value="item1"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| <Toggle value="item2" variant="accent"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| <Toggle value="item3"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
|
|
||
| <Toggle value="item4" variant="accent"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| <Toggle value="item5"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| <Toggle value="item6" variant="accent"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| </ToggleGroup> | ||
| </Grid.Root> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| const renderCases = ({ size, disabled }: ToggleGroup.Props) => { | ||
| return ( | ||
| <ToggleGroup size={size} value={['item3']} disabled={disabled}> | ||
| <Toggle value="item1"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| <Toggle value="item2"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| <Toggle value="item3"> | ||
| <HeartIcon /> | ||
| </Toggle> | ||
| </ToggleGroup> | ||
| ); | ||
| }; |
195 changes: 195 additions & 0 deletions
195
packages/core/src/components/toggle-group/toggle-group.test.tsx
This file contains hidden or 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,195 @@ | ||
| import { render } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { axe } from 'vitest-axe'; | ||
|
|
||
| import { ToggleGroup } from '.'; | ||
| import { Toggle } from '../toggle'; | ||
|
|
||
| const ToggleGroupTest = (props: ToggleGroup.Props) => ( | ||
| <ToggleGroup {...props}> | ||
| <Toggle value="a" aria-label="a" /> | ||
| <Toggle value="b" aria-label="b" /> | ||
| <Toggle value="c" aria-label="c" /> | ||
| </ToggleGroup> | ||
| ); | ||
|
|
||
| describe('<ToggleGroup>', () => { | ||
| it('should have no a11y violations', async () => { | ||
| const rendered = render(<ToggleGroupTest />); | ||
| const result = await axe(rendered.container); | ||
|
|
||
| expect(result).toHaveNoViolations(); | ||
| }); | ||
|
|
||
| it('should press a toggle when clicked', async () => { | ||
| const rendered = render(<ToggleGroupTest />); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
|
|
||
| await userEvent.click(toggleA); | ||
|
|
||
| expect(toggleA).toHaveAttribute('aria-pressed', 'true'); | ||
| }); | ||
|
|
||
| it('should unpress the previously pressed toggle when another is clicked', async () => { | ||
| const rendered = render(<ToggleGroupTest />); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
| const toggleB = rendered.getByRole('button', { name: 'b' }); | ||
|
|
||
| await userEvent.click(toggleA); | ||
| await userEvent.click(toggleB); | ||
|
|
||
| expect(toggleA).toHaveAttribute('aria-pressed', 'false'); | ||
| expect(toggleB).toHaveAttribute('aria-pressed', 'true'); | ||
| }); | ||
|
|
||
| describe('prop: defaultValue', () => { | ||
| it('should mark the matching toggle as pressed', () => { | ||
| const rendered = render(<ToggleGroupTest defaultValue={['b']} />); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
| const toggleB = rendered.getByRole('button', { name: 'b' }); | ||
|
|
||
| expect(toggleA).toHaveAttribute('aria-pressed', 'false'); | ||
| expect(toggleB).toHaveAttribute('aria-pressed', 'true'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('prop: value', () => { | ||
| it('should reflect the controlled value', () => { | ||
| const rendered = render(<ToggleGroupTest value={['c']} />); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
| const toggleC = rendered.getByRole('button', { name: 'c' }); | ||
|
|
||
| expect(toggleA).toHaveAttribute('aria-pressed', 'false'); | ||
| expect(toggleC).toHaveAttribute('aria-pressed', 'true'); | ||
| }); | ||
|
|
||
| it('should not change pressed state without onValueChange update', async () => { | ||
| const rendered = render(<ToggleGroupTest value={[]} />); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
|
|
||
| await userEvent.click(toggleA); | ||
|
|
||
| expect(toggleA).toHaveAttribute('aria-pressed', 'false'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('prop: onValueChange', () => { | ||
| it('should call onValueChange with the new value array when clicked', async () => { | ||
| const onValueChange = vi.fn(); | ||
| const rendered = render(<ToggleGroupTest onValueChange={onValueChange} />); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
|
|
||
| await userEvent.click(toggleA); | ||
|
|
||
| expect(onValueChange).toHaveBeenCalledTimes(1); | ||
| expect(onValueChange).toHaveBeenCalledWith(['a'], expect.anything()); | ||
| }); | ||
|
|
||
| it('should call onValueChange with an empty array when the pressed toggle is clicked again', async () => { | ||
| const onValueChange = vi.fn(); | ||
| const rendered = render( | ||
| <ToggleGroupTest defaultValue={['a']} onValueChange={onValueChange} />, | ||
| ); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
|
|
||
| await userEvent.click(toggleA); | ||
|
|
||
| expect(toggleA).toHaveAttribute('aria-pressed', 'false'); | ||
| expect(onValueChange).toHaveBeenCalledTimes(1); | ||
| expect(onValueChange).toHaveBeenCalledWith([], expect.anything()); | ||
| }); | ||
| }); | ||
|
|
||
| describe('prop: multiple', () => { | ||
| it('should allow multiple toggles to be pressed at the same time', async () => { | ||
| const rendered = render(<ToggleGroupTest multiple />); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
| const toggleB = rendered.getByRole('button', { name: 'b' }); | ||
|
|
||
| await userEvent.click(toggleA); | ||
| await userEvent.click(toggleB); | ||
|
|
||
| expect(toggleA).toHaveAttribute('aria-pressed', 'true'); | ||
| expect(toggleB).toHaveAttribute('aria-pressed', 'true'); | ||
| }); | ||
|
|
||
| it('should call onValueChange with the new value array when multiple toggles are pressed', async () => { | ||
| const onValueChange = vi.fn(); | ||
| const rendered = render(<ToggleGroupTest multiple onValueChange={onValueChange} />); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
| const toggleB = rendered.getByRole('button', { name: 'b' }); | ||
|
|
||
| await userEvent.click(toggleA); | ||
| await userEvent.click(toggleB); | ||
|
|
||
| expect(onValueChange).toHaveBeenCalledTimes(2); | ||
| expect(onValueChange).toHaveBeenNthCalledWith(1, ['a'], expect.anything()); | ||
| expect(onValueChange).toHaveBeenNthCalledWith(2, ['a', 'b'], expect.anything()); | ||
| }); | ||
| }); | ||
|
|
||
| describe('prop: disabled', () => { | ||
| it('should disable all toggles', () => { | ||
| const rendered = render(<ToggleGroupTest disabled />); | ||
| const toggles = rendered.getAllByRole('button'); | ||
|
|
||
| toggles.forEach((toggle) => { | ||
| expect(toggle).toBeDisabled(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should not call onValueChange when a toggle is clicked', async () => { | ||
| const onValueChange = vi.fn(); | ||
| const rendered = render(<ToggleGroupTest disabled onValueChange={onValueChange} />); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
|
|
||
| await userEvent.click(toggleA); | ||
|
|
||
| expect(onValueChange).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('keyboard navigation', () => { | ||
| it('should focus the next toggle when pressing ArrowRight', async () => { | ||
| const rendered = render(<ToggleGroupTest />); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
| const toggleB = rendered.getByRole('button', { name: 'b' }); | ||
|
|
||
| toggleA.focus(); | ||
| await userEvent.keyboard('[ArrowRight]'); | ||
|
|
||
| expect(toggleB).toHaveFocus(); | ||
| }); | ||
|
|
||
| it('should focus the previous toggle when pressing ArrowLeft', async () => { | ||
| const rendered = render(<ToggleGroupTest />); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
| const toggleB = rendered.getByRole('button', { name: 'b' }); | ||
|
|
||
| toggleB.focus(); | ||
| await userEvent.keyboard('[ArrowLeft]'); | ||
|
|
||
| expect(toggleA).toHaveFocus(); | ||
| }); | ||
|
|
||
| it('should focus multiple toggles when pressing ArrowRight multiple times', async () => { | ||
| const onValueChange = vi.fn(); | ||
| const rendered = render(<ToggleGroupTest multiple onValueChange={onValueChange} />); | ||
| const toggleA = rendered.getByRole('button', { name: 'a' }); | ||
| const toggleB = rendered.getByRole('button', { name: 'b' }); | ||
| const toggleC = rendered.getByRole('button', { name: 'c' }); | ||
|
|
||
| toggleA.focus(); | ||
| await userEvent.keyboard('[ArrowRight]'); | ||
| await userEvent.keyboard('[Space]'); | ||
| await userEvent.keyboard('[ArrowRight]'); | ||
| await userEvent.keyboard('[Space]'); | ||
|
|
||
| expect(toggleB).toHaveAttribute('aria-pressed', 'true'); | ||
| expect(toggleC).toHaveAttribute('aria-pressed', 'true'); | ||
| expect(onValueChange).toHaveBeenCalledTimes(2); | ||
| expect(onValueChange).toHaveBeenNthCalledWith(1, ['b'], expect.anything()); | ||
| expect(onValueChange).toHaveBeenNthCalledWith(2, ['b', 'c'], expect.anything()); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.