Skip to content
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

feat(ToggleGroup): ✨ New ToggleGroup component #813

Merged
merged 43 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7365585
create dumb ToggleGroupComponent
Magnusrm Sep 13, 2023
8a905de
implement controlled ToggleGroup
Magnusrm Sep 14, 2023
538025b
implement uncontrolled ToggleGroup
Magnusrm Sep 14, 2023
b173c5d
implement roving tab index
Magnusrm Sep 15, 2023
eb49374
add some js doc and forward ref functionality
Magnusrm Sep 20, 2023
4e3e7e3
update stories
Magnusrm Sep 20, 2023
5b5dbc0
add some tests
Magnusrm Sep 20, 2023
623163f
further update storybook
Magnusrm Sep 20, 2023
0204122
add test for RovingTabindexRoot
Magnusrm Sep 20, 2023
74d104b
use OverridableComponent for RovingTabindexRoot, solve typeerrors
Magnusrm Sep 25, 2023
ba3d3fc
make RovingTabindexItem as reusable roving item
Magnusrm Sep 26, 2023
229b093
use `RovingTabindexItem` in `ToggleGroupItem`
Magnusrm Sep 26, 2023
8ef30b9
add some comments
Magnusrm Sep 26, 2023
029ea65
some comments
Magnusrm Sep 26, 2023
082f8cf
add another test for RovingTabindexRoot
Magnusrm Sep 27, 2023
071dcda
fix width issue on last item active
Magnusrm Sep 27, 2023
b14b3c6
some cleanup
Magnusrm Sep 27, 2023
6de1097
make Item value fallback to children
Magnusrm Sep 27, 2023
3a18b61
Merge branch 'main' into feat/toggle-group-component
Magnusrm Sep 27, 2023
ce5fbbd
fix typo
Magnusrm Sep 27, 2023
0fb6a9c
add selected value to storybook
Magnusrm Sep 28, 2023
b844d07
add only icon example
Magnusrm Sep 28, 2023
277d439
more diverseity in icons, storybook
Magnusrm Sep 28, 2023
94cfb73
make only icon items rectangular
Magnusrm Sep 28, 2023
5a7cc8d
make focus appear only on focus-visible
Magnusrm Sep 28, 2023
b4cc0d6
remove focus last item demo, since it does not work with only :focus-…
Magnusrm Sep 28, 2023
f819568
Update packages/react/src/components/ToggleGroup/ToggleGroup.tsx
Magnusrm Sep 28, 2023
90a83de
Update packages/react/src/components/ToggleGroup/ToggleGroup.tsx
Magnusrm Sep 28, 2023
ce7047b
Update packages/react/src/components/ToggleGroup/ToggleGroup.tsx
Magnusrm Sep 28, 2023
55583c4
Update packages/react/src/components/ToggleGroup/ToggleGroup.tsx
Magnusrm Sep 28, 2023
046abfc
Update packages/react/src/components/ToggleGroup/ToggleGroup.tsx
Magnusrm Sep 28, 2023
d1dbe3d
Update packages/react/src/components/ToggleGroup/ToggleGroup.tsx
Magnusrm Sep 28, 2023
5e797e1
Update packages/react/src/components/ToggleGroup/ToggleGroupItem/useT…
Magnusrm Sep 28, 2023
b281024
Update packages/react/src/components/ToggleGroup/ToggleGroup.stories.tsx
Magnusrm Sep 28, 2023
87ce92a
adhere to review comments
Magnusrm Sep 28, 2023
dcc9be6
Merge branch 'main' into feat/toggle-group-component
Magnusrm Sep 28, 2023
c6ae9f9
use one language in storybook example
Magnusrm Sep 28, 2023
f3a4f74
reintroduce fix for focus being overlapped
Magnusrm Sep 28, 2023
bb9b3ae
Merge branch 'main' into feat/toggle-group-component
Magnusrm Sep 28, 2023
36af35a
remove fix for focus-visible
Magnusrm Sep 28, 2023
470f571
Update packages/react/src/components/ToggleGroup/ToggleGroup.stories.tsx
Magnusrm Sep 28, 2023
dcf8b34
Update packages/react/src/components/ToggleGroup/ToggleGroup.mdx
Magnusrm Sep 28, 2023
69d40e5
update storybook preview props
Magnusrm Sep 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/Overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ V1 er klar når følgende komponenter er markert som "✅ Felles":
| [Tag](/docs/felles-tag--docs) | ✅ Felles | [Figma - Tag](https://www.figma.com/file/vpM9dqqQPHqU6ogfKp5tlr/Felles-komponenter?node-id=10185%3A59053&t=7Q2N4sUdQGhFZrPh-1) | [Github - Tag](https://github.com/digdir/designsystem/issues/322) |
| [Textarea](/docs/felles-textarea--docs) | ✅ Felles | [Figma - Text area](https://www.figma.com/file/vpM9dqqQPHqU6ogfKp5tlr/Felles-komponenter?node-id=6632%3A21873&t=7Q2N4sUdQGhFZrPh-1) | [Github - Textarea](https://github.com/digdir/designsystem/issues/323) |
| [Textfield](/docs/felles-textfield--docs) | ✅ Felles | [Figma - Text Field](https://www.figma.com/file/vpM9dqqQPHqU6ogfKp5tlr/Felles-komponenter?node-id=6632%3A22228&t=7Q2N4sUdQGhFZrPh-1) | [Github - Textfield](https://github.com/digdir/designsystem/issues/92) |
| [ToggleGroup](/docs/altinn-togglebuttongroup--docs) | 🔵 Altinn | Figma - Toggle Group | [Github - ToggleGroup](https://github.com/digdir/designsystem/issues/304) |
| [ToggleGroup](/docs/felles-togglegroup--docs) | ✅ Felles | Figma - Toggle Group | [Github - ToggleGroup](https://github.com/digdir/designsystem/issues/304) |
| Tooltip | 🚸 Ikke påbegynt | Figma - Tooltip | [Github - Tooltip](https://github.com/digdir/designsystem/issues/93) |
| [Typography](/docs/felles-typography--docs) | ✅ Felles | [Figma - Typography](https://www.figma.com/file/vpM9dqqQPHqU6ogfKp5tlr/Felles-komponenter?node-id=9219%3A49405&t=7Q2N4sUdQGhFZrPh-1) | [Github - Typography](https://github.com/digdir/designsystem/issues/324) |

Expand Down
35 changes: 35 additions & 0 deletions packages/react/src/components/ToggleGroup/ToggleGroup.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Meta, Canvas, Story, Controls, Primary } from '@storybook/blocks';
import { Information } from '../../../../../docs-components';
import * as ToggleGroupStories from './ToggleGroup.stories';

<Meta of={ToggleGroupStories} />

# ToggleGroup

Description of the ToggleGroup component.

<Primary />
<Controls />

## Bruk

<Information text='token' />

```tsx
import '@digdir/design-system-tokens/brand/altinn/tokens.css'; // Importeres kun en gang i appen din.
import { ToggleGroup } from '@digdir/design-system-react';

<ToggleGroup defaultValue="value1">
<ToggleGroup value="value1">Option 1</ToggleGroup.Item>
<ToggleGroup value="value2">Option 2</ToggleGroup.Item>
<ToggleGroup value="value3">Option 3</ToggleGroup.Item>
</ToggleGroup>;
```

## Only Icons

<Canvas of={ToggleGroupStories.OnlyIcons} />

## Controlled

<Canvas of={ToggleGroupStories.Controlled} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.toggleGroupContainer {
background-color: var(--fds-semantic-background-default);
border: var(--fds-semantic-border-neutral-default) solid var(--fds-border_width-default);
border-radius: var(--fds-border_radius-medium);
}

.groupContent {
display: inline-grid;
gap: var(--fds-spacing-1);
grid-auto-columns: 1fr;
grid-auto-flow: column;
padding: var(--fds-spacing-1);
}
117 changes: 117 additions & 0 deletions packages/react/src/components/ToggleGroup/ToggleGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, { useState } from 'react';
import type { Meta, StoryFn } from '@storybook/react';
import * as icons from '@navikt/aksel-icons';

import { Button } from '../Button';

import { ToggleGroup } from '.';

const icon = (
<svg
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M12 0c6.627 0 12 5.373 12 12s-5.373 12-12 12S0 18.627 0 12 5.373 0 12 0Zm0 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm5.047 5.671 1.399 1.43-8.728 8.398L6 14.02l1.395-1.434 2.319 2.118 7.333-7.032Z'
fill='currentColor'
/>
</svg>
);

const AkselIcon = icons.AirplaneFillIcon;
const AkselIcon2 = icons.NewspaperFillIcon;
const AkselIcon3 = icons.BrailleIcon;
const AkselIcon4 = icons.BackpackFillIcon;

export default {
title: 'Felles/ToggleGroup',
component: ToggleGroup,
} as Meta;

export const Preview: StoryFn<typeof ToggleGroup> = (args) => {
return (
<ToggleGroup {...args}>
<ToggleGroup.Item>Peanut</ToggleGroup.Item>
<ToggleGroup.Item>Walnut</ToggleGroup.Item>
<ToggleGroup.Item>Pistachio 🤤</ToggleGroup.Item>
</ToggleGroup>
);
};

Preview.args = {
defaultValue: 'Peanut',
size: 'medium',
name: 'toggle-group-nuts',
};

export const OnlyIcons: StoryFn<typeof ToggleGroup> = () => {
const handleChange = (value: string) => {
console.log(value);
};

return (
<ToggleGroup
defaultValue={'option-1'}
onChange={handleChange}
>
<ToggleGroup.Item
value={'option-1'}
icon={<AkselIcon3 />}
/>
<ToggleGroup.Item
value={'option-2'}
icon={<AkselIcon2 />}
/>
<ToggleGroup.Item
value={'option-3'}
icon={<AkselIcon4 />}
/>
</ToggleGroup>
);
};

export const Controlled: StoryFn<typeof ToggleGroup> = () => {
const [value, setValue] = useState<string>('peanut');
return (
<>
<div style={{ display: 'flex', gap: '4px' }}>
<Button
size='small'
onClick={() => setValue('peanut')}
>
Select Peanut
</Button>
</div>
<br />
<ToggleGroup
value={value}
size='medium'
onChange={setValue}
>
<ToggleGroup.Item
value='pistachio'
icon={<AkselIcon />}
>
Pistachio
</ToggleGroup.Item>
<ToggleGroup.Item
value='peanut'
icon={icon}
>
Peanut
</ToggleGroup.Item>
<ToggleGroup.Item
value='walnut'
icon={<AkselIcon2 />}
>
Walnut
</ToggleGroup.Item>
</ToggleGroup>
<br />
<span>You have chosen: {value}</span>
</>
);
};
131 changes: 131 additions & 0 deletions packages/react/src/components/ToggleGroup/ToggleGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { ToggleGroup } from '.';

const user = userEvent.setup();

describe('ToggleGroup', () => {
test('has generated name for ToggleGroupItem children', () => {
render(
<ToggleGroup>
<ToggleGroup.Item value='test'>test</ToggleGroup.Item>
</ToggleGroup>,
);

const item = screen.getByRole('radio');
expect(item).toHaveAttribute('name');
});

test('has passed name to ToggleGroupItem children', (): void => {
render(
<ToggleGroup name='my name'>
<ToggleGroup.Item value='test'>test</ToggleGroup.Item>
</ToggleGroup>,
);

const item = screen.getByRole<HTMLButtonElement>('radio');
expect(item.name).toEqual('my name');
});
test('has passed size to ToggleGroupItem children', (): void => {
render(
<ToggleGroup size='medium'>
<ToggleGroup.Item value='test'>test</ToggleGroup.Item>
</ToggleGroup>,
);

const item = screen.getByRole<HTMLButtonElement>('radio');
expect(item).toHaveClass('medium');
});
test('can navigate with tab and arrow keys', async () => {
render(
<ToggleGroup>
<ToggleGroup.Item value='test'>test</ToggleGroup.Item>
<ToggleGroup.Item value='test2'>test2</ToggleGroup.Item>
<ToggleGroup.Item value='test3'>test3</ToggleGroup.Item>
</ToggleGroup>,
);

const item1 = screen.getByRole<HTMLButtonElement>('radio', {
name: 'test',
});
const item2 = screen.getByRole<HTMLButtonElement>('radio', {
name: 'test2',
});
const item3 = screen.getByRole<HTMLButtonElement>('radio', {
name: 'test3',
});
await user.tab();
expect(item1).toHaveFocus();
await user.type(item1, '{arrowright}');
expect(item2).toHaveFocus();
await user.type(item2, '{arrowright}');
expect(item3).toHaveFocus();
await user.type(item3, '{arrowleft}');
expect(item2).toHaveFocus();
});
test('has correct ToggleGroupItem defaultChecked & checked when defaultValue is used', () => {
render(
<ToggleGroup defaultValue='test2'>
<ToggleGroup.Item value='test1'>test1</ToggleGroup.Item>
<ToggleGroup.Item value='test2'>test2</ToggleGroup.Item>
<ToggleGroup.Item value='test3'>test3</ToggleGroup.Item>
</ToggleGroup>,
);

const item = screen.getByRole<HTMLButtonElement>('radio', {
name: 'test2',
});
expect(item).toHaveAttribute('aria-checked', 'true');
});
test('has passed clicked ToggleGroupItem element to onChange', async () => {
let onChangeValue = '';

render(
<ToggleGroup onChange={(value) => (onChangeValue = value)}>
<ToggleGroup.Item value='test1'>test1</ToggleGroup.Item>
<ToggleGroup.Item value='test2'>test2</ToggleGroup.Item>
</ToggleGroup>,
);

const item = screen.getByRole<HTMLButtonElement>('radio', {
name: 'test2',
});

expect(item).toHaveAttribute('aria-checked', 'false');

await user.click(item);

expect(onChangeValue).toEqual('test2');
expect(item).toHaveAttribute('aria-checked', 'true');
});
test('has passed clicked ToggleGroupItem element to onChange when defaultValue is used', async () => {
let onChangeValue = '';

render(
<ToggleGroup
defaultValue='test1'
onChange={(value) => (onChangeValue = value)}
>
<ToggleGroup.Item value='test1'>test1</ToggleGroup.Item>
<ToggleGroup.Item value='test2'>test2</ToggleGroup.Item>
</ToggleGroup>,
);

const item1 = screen.getByRole<HTMLButtonElement>('radio', {
name: 'test1',
});
const item2 = screen.getByRole<HTMLButtonElement>('radio', {
name: 'test2',
});

expect(item1).toHaveAttribute('aria-checked', 'true');
expect(item2).toHaveAttribute('aria-checked', 'false');

await user.click(item2);

expect(onChangeValue).toEqual('test2');
expect(item2).toHaveAttribute('aria-checked', 'true');
});
});
89 changes: 89 additions & 0 deletions packages/react/src/components/ToggleGroup/ToggleGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { HTMLAttributes } from 'react';
import React, { createContext, forwardRef, useId, useState } from 'react';
import cn from 'classnames';

import { RovingTabindexRoot } from '../../utility-components/RovingTabIndex';

import classes from './ToggleGroup.module.css';

export type ToggleGroupContextProps = {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
name?: string;
size?: 'small' | 'medium' | 'large';
};

export const ToggleGroupContext = createContext<ToggleGroupContextProps>({});

export type ToggleGroupProps = {
/** Controlled state for `ToggleGroup` component. */
value?: string;
/** Default value. */
defaultValue?: string;
/** Callback with selected `ToggleGroupItem` `value` */
onChange?: (value: string) => void;
/** Form element name */
name?: string;
/** Changes items size and paddings */
size?: 'small' | 'medium' | 'large';
} & Omit<HTMLAttributes<HTMLDivElement>, 'value' | 'onChange'>;

Magnusrm marked this conversation as resolved.
Show resolved Hide resolved
/** `ToggleGroup` component.
* @example
* ```tsx
* <ToggleGroup onChange={(value) => console.log(value)}>
* <ToggleGroup.Item value='1'>Toggle 1</ToggleGroup.Item>
* <ToggleGroup.Item value='2'>Toggle 2</ToggleGroup.Item>
* <ToggleGroup.Item value='3'>Toggle 3</ToggleGroup.Item>
* </ToggleGroup>
* ```
*/
export const ToggleGroup = forwardRef<HTMLDivElement, ToggleGroupProps>(
(
{ children, value, defaultValue, onChange, size = 'medium', name, ...rest },
ref,
) => {
const nameId = useId();
const isControlled = value !== undefined;
const [uncontrolledValue, setUncontrolledValue] = useState<
string | undefined
>(defaultValue);

let onValueChange = onChange;
if (!isControlled) {
onValueChange = (newValue: string) => {
setUncontrolledValue(newValue);
onChange?.(newValue);
};
value = uncontrolledValue;
}

return (
<div
{...rest}
className={cn(classes.toggleGroupContainer, rest.className)}
ref={ref}
>
<ToggleGroupContext.Provider
value={{
value,
defaultValue,
name: name ?? `togglegroup-name-${nameId}`,
onChange: onValueChange,
size,
}}
>
<RovingTabindexRoot
as='div'
valueId={value}
className={classes.groupContent}
role='radiogroup'
>
{children}
</RovingTabindexRoot>
</ToggleGroupContext.Provider>
</div>
);
},
);
Loading
Loading