Skip to content

Commit

Permalink
chore(Chip): add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
eirikbacker committed Sep 24, 2024
1 parent 844d142 commit 36159ce
Show file tree
Hide file tree
Showing 17 changed files with 185 additions and 509 deletions.
12 changes: 8 additions & 4 deletions apps/theme/components/Previews/Components/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,15 @@ export const Components = () => {
Filtrer på språk
</Heading>
<div className={classes.chips}>
<Chip.Toggle selected checkmark={false} size='sm'>
<Chip.Radio name='language' size='sm' checked>
Bokmål
</Chip.Toggle>
<Chip.Toggle size='sm'>Nynorsk</Chip.Toggle>
<Chip.Toggle size='sm'>Engelsk</Chip.Toggle>
</Chip.Radio>
<Chip.Radio name='language' size='sm'>
Nynorsk
</Chip.Radio>
<Chip.Radio name='language' size='sm'>
Engelsk
</Chip.Radio>
</div>
</div>
<div className={cl(classes.card, classes.comboBox)}>
Expand Down
1 change: 1 addition & 0 deletions packages/css/chip.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
--dsc-chip-padding: 0 var(--ds-spacing-3);

@composes ds-focus from './utilities.css';
@composes ds-paragraph-short from './baseline/typography.css';

align-items: center;
background: var(--dsc-chip-background);
Expand Down
36 changes: 7 additions & 29 deletions packages/react/src/components/Chip/Chip.mdx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { Meta, Canvas, Controls, Primary } from '@storybook/blocks';

import * as ChipStories from './Chip.stories';
import * as ToggleChipStories from './Toggle/Toggle.stories';
import * as RemovableChipStories from './Removable/Removable.stories';
import * as GroupChipStories from './Group/Group.stories';

<Meta of={ChipStories} />

Expand All @@ -23,33 +20,18 @@ import * as GroupChipStories from './Group/Group.stories';
```tsx
import { Chip } from '@digdir/designsystemet-react';

<Chip.Toggle>You are using the Chip component!</Chip.Toggle>;
<Chip.Button>You are using the Chip component!</Chip.Button>;
```

<br/>

## Eksempler

<br/>
### `Chip.Toggle`
`Chip.Toggle` kan brukes som et alternativ til `Button`. Den brukes når handlingen(e) er direkte relatert til hovedinnholdet.
<Canvas of={ChipStories.Preview} />
<br/>

### `Chip.Group`

Denne kodeblokken viser hvordan du grupperer `Chip` ved hjelp av `Chip.Group`.

<Canvas of={GroupChipStories.Preview} />
<Controls of={GroupChipStories.Preview} />

<br/>

### `Chip.Toggle` med `selected=true`
Vi bruker `Chip` med hake som alternativer til vanlige radiobuttons og checkboxer. Brukes til filtrering av innhold og data. Innholdet tilpasser seg kategoriene som blir valgt.

<Canvas of={GroupChipStories.CheckGroup} />

### `Chip.Radio` og `Chip.Checkbox`
`Chip.Radio` kan brukes som et alternativ til `Button`. Den brukes når handlingen(e) er direkte relatert til hovedinnholdet.
<Canvas of={ChipStories.Checkbox} />
<Canvas of={ChipStories.Radio} />
<br/>

### `Chip.Removable`
Expand All @@ -58,12 +40,8 @@ Vi bruker `Chip` med kryss når vi vil at brukerne skal kunne fjerne valgte verd
Denne komponenten inneholder et kryss som indikerer at filteret kan fjernes. Det er viktig å merke seg at `aria-label` må legges til dersom innholdet
i Chip ikke forklarer at den kan fjernes. I dette eksempelet er det lagt til `aria-label="Slett {land}"`.

<Canvas of={GroupChipStories.RemoveGroup} />
<Controls of={RemovableChipStories.Preview} />
<Canvas of={ChipStories.Removable} />
<br/>



<br/>

## Retningslinjer for `Chip`
Expand All @@ -89,4 +67,4 @@ Chips bør ha så få ord som mulig, helst bare ett eller to.
<br/>

## Tilgjengelighet
Hvis deler av en side blir oppdatert samtidig som brukerne gjør noe på siden, må vi passe på at brukere med skjermleser får beskjed om det. Vi kan bruke en `ARIA live region` til det, som kan vise antall treff eller andre endringer.
Hvis deler av en side blir oppdatert samtidig som brukerne gjør noe på siden, må vi passe på at brukere med skjermleser får beskjed om det. Vi kan bruke en `ARIA live region` til det, som kan vise antall treff eller andre endringer.
8 changes: 6 additions & 2 deletions packages/react/src/components/Chip/Chip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,20 @@ export const Removable: StoryFn<typeof Chip.Removable> = (args) => (
<Chip.Removable {...args}>Nynorsk</Chip.Removable>
);

Removable.args = {
'aria-label': 'Slett Nynorsk',
};

export const Checkbox: StoryFn<typeof Chip.Checkbox> = (args) => (
<Chip.Checkbox {...args}>Nynorsk</Chip.Checkbox>
);

export const Radio: StoryFn<typeof Chip.Radio> = (args) => (
<>
<Chip.Radio name='radio' value='nynorsk' {...args}>
<Chip.Radio {...args} name='my-radio' value='nynorsk' defaultChecked>
Nynorsk
</Chip.Radio>
<Chip.Radio name='radio' value='bokmål' {...args}>
<Chip.Radio {...args} name='my-radio' value='bokmål'>
Bokmål
</Chip.Radio>
</>
Expand Down
117 changes: 117 additions & 0 deletions packages/react/src/components/Chip/Chips.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { render, screen } from '@testing-library/react';
import { Chip } from '..';

describe('Chip.Button', () => {
it('should render button as native element', () => {
render(<Chip.Button>Norwegian</Chip.Button>);
const chip = screen.getByRole('button', { name: 'Norwegian' });

expect(chip).toBeInTheDocument();
});

it('rest props should be supported', () => {
render(<Chip.Button className='testClass'>Norwegian</Chip.Button>);

const chip = screen.getByRole('button', { name: 'Norwegian' });
expect(chip).toHaveClass('testClass');

// Ensure that the last class is the one added by rest-props.
const lastClassNameIndex = chip.classList.length - 1;
expect(chip.classList[lastClassNameIndex]).toBe('testClass');
});
});

describe('Chip.Removable', () => {
it('should render button as native element', () => {
render(<Chip.Removable>Norwegian</Chip.Removable>);

const chip = screen.getByRole('button', { name: 'Norwegian' });
expect(chip).toBeInTheDocument();
expect(chip).toHaveAttribute('data-removable');
});

it('rest props should be supported', () => {
render(<Chip.Removable className='testClass'>Norwegian</Chip.Removable>);

const chip = screen.getByRole('button', { name: 'Norwegian' });
expect(chip).toHaveClass('testClass');

// Ensure that the last class is the one added by rest-props.
const lastClassNameIndex = chip.classList.length - 1;
expect(chip.classList[lastClassNameIndex]).toBe('testClass');
});
});

describe('Chip.Checkbox', () => {
it('should render label and input as native element', () => {
render(<Chip.Checkbox>Norwegian</Chip.Checkbox>);

const chip = screen.getByLabelText('Norwegian');
const input = screen.getByRole('checkbox');
expect(chip).toBeInTheDocument();
expect(input).toBeInTheDocument();
});

it('rest props should be supported', () => {
render(
<Chip.Checkbox
checked
className='testClass'
disabled
name='language'
value='norwegian'
>
Norwegian
</Chip.Checkbox>,
);

const chip = screen.getByText('Norwegian', { selector: 'label' });
const input = screen.getByRole('checkbox');
expect(chip).toHaveClass('testClass');
expect(input).toHaveAttribute('name', 'language');
expect(input).toHaveAttribute('value', 'norwegian');
expect(input).toBeChecked();
expect(input).toBeDisabled();

// Ensure that the last class is the one added by rest-props.
const lastClassNameIndex = chip.classList.length - 1;
expect(chip.classList[lastClassNameIndex]).toBe('testClass');
});
});

describe('Chip.Radio', () => {
it('should render label and input as native element', () => {
render(<Chip.Radio>Norwegian</Chip.Radio>);

const chip = screen.getByLabelText('Norwegian');
const input = screen.getByRole('radio');
expect(chip).toBeInTheDocument();
expect(input).toBeInTheDocument();
});

it('rest props should be supported', () => {
render(
<Chip.Radio
checked
className='testClass'
disabled
name='language'
value='norwegian'
>
Norwegian
</Chip.Radio>,
);

const chip = screen.getByText('Norwegian', { selector: 'label' });
const input = screen.getByRole('radio');
expect(chip).toHaveClass('testClass');
expect(input).toHaveAttribute('name', 'language');
expect(input).toHaveAttribute('value', 'norwegian');
expect(input).toBeChecked();
expect(input).toBeDisabled();

// Ensure that the last class is the one added by rest-props.
const lastClassNameIndex = chip.classList.length - 1;
expect(chip.classList[lastClassNameIndex]).toBe('testClass');
});
});
112 changes: 42 additions & 70 deletions packages/react/src/components/Chip/Chips.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
import { Slot, Slottable } from '@radix-ui/react-slot';
import cl from 'clsx/lite';
import { forwardRef } from 'react';
import type {
ButtonHTMLAttributes,
ForwardedRef,
InputHTMLAttributes,
LabelHTMLAttributes,
} from 'react';
import type { ButtonHTMLAttributes, InputHTMLAttributes } from 'react';
import { Paragraph } from '../Typography';

type Button = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'>;
type Label = LabelHTMLAttributes<HTMLLabelElement> & {
name?: string;
value?: string;
checked?: boolean;
disabled?: boolean;
};
type ChipBaseProps = {
/**
* Size
Expand All @@ -29,93 +17,77 @@ type ChipBaseProps = {
asChild?: boolean;
};

export type ChipButtonProps = ChipBaseProps & Button;
export type ChipRemovableProps = ChipBaseProps & Button;
export type ChipCheckboxProps = ChipBaseProps & Label;
export type ChipRadioProps = ChipBaseProps & Label;

const render = <
I extends InputHTMLAttributes<HTMLInputElement> | undefined,
T extends I extends undefined ? HTMLButtonElement : HTMLLabelElement,
R extends I extends undefined ? ChipButtonProps : ChipRadioProps,
>(
{ asChild, className, children, size = 'md', ...rest }: R,
ref: ForwardedRef<T>,
input?: I,
) => {
const tagName: string = input ? 'label' : 'button';
const Component = asChild ? Slot : tagName;

return (
<Paragraph size={size} asChild>
<Component
className={cl('ds-chip', className)}
data-size={size}
type={asChild || input ? undefined : 'button'}
ref={ref}
{...rest}
>
{input && <input {...input} />}
<Slottable>{children}</Slottable>
</Component>
</Paragraph>
);
};
export type ChipRemovableProps = ChipButtonProps;
export type ChipRadioProps = ChipCheckboxProps;
export type ChipButtonProps = ChipBaseProps &
ButtonHTMLAttributes<HTMLButtonElement>;
export type ChipCheckboxProps = ChipBaseProps &
Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'>;

/**
* Chip.Button used for interaction
* @example
* <Chip.Button>Click me</Chip.Button>
*/
export const ChipButton = forwardRef<HTMLButtonElement, ChipButtonProps>(
function ChipButton(props, ref) {
return render(props, ref);
function ChipButton({ asChild, className, size, ...rest }, ref) {
const Component = asChild ? Slot : 'button';

return (
<Component
className={cl('ds-chip', className)}
data-size={size}
type={asChild ? undefined : 'button'}
ref={ref}
{...rest}
/>
);
},
);

/**
* Chip.Removable used for interaction
* @example
* <Chip.Removable>Click me</Chip.Removable>
* <Chip.Removable>Click to remove me</Chip.Removable>
*/
export const ChipRemovable = forwardRef<HTMLButtonElement, ChipButtonProps>(
export const ChipRemovable = forwardRef<HTMLButtonElement, ChipRemovableProps>(
function ChipRemovable(props, ref) {
return render({ 'data-removable': true, ...props }, ref);
return <ChipButton data-removable ref={ref} {...props}></ChipButton>;
},
);

/**
* Chip.Checkbox used for multiselection
* @example
* <Chip.Checkbox name="language">Nynorsk</Chip.Checkbox>
* <Chip.Checkbox name="language">Bokmål</Chip.Checkbox>
* <Chip.Checkbox name="language" value="nynorsk">Nynorsk</Chip.Checkbox>
* <Chip.Checkbox name="language" value="bokmål">Bokmål</Chip.Checkbox>
*/
export const ChipCheckbox = forwardRef<HTMLLabelElement, ChipCheckboxProps>(
function ChipCheckbox({ checked, name, value, disabled, ...props }, ref) {
return render(props, ref, {
checked,
disabled,
name,
type: 'checkbox',
value,
});
function ChipCheckbox({ asChild, children, className, size, ...rest }, ref) {
const inputType = (rest as { type?: string }).type ?? 'checkbox';
const Component = asChild ? Slot : 'label';

return (
<Component
className={cl('ds-chip', className)}
data-size={size}
ref={ref}
>
<input {...rest} type={inputType} />
<Slottable>{children}</Slottable>
</Component>
);
},
);

/**
* Chip.Radio used for single selection
* @example
* <Chip.Radio name="language">Nynorsk</Chip.Radio>
* <Chip.Radio name="language">Bokmål</Chip.Radio>
* <Chip.Radio name="language" value="nynorsk">Nynorsk</Chip.Radio>
* <Chip.Radio name="language" value="bokmål">Bokmål</Chip.Radio>
*/
export const ChipRadio = forwardRef<HTMLLabelElement, ChipRadioProps>(
function ChipRadio({ name, value, checked, disabled, ...props }, ref) {
return render(props, ref, {
checked,
disabled,
name,
type: 'radio',
value,
});
function ChipRadio(props, ref) {
return <ChipCheckbox {...{ ref, type: 'radio', ...props }} />;
},
);
Loading

0 comments on commit 36159ce

Please sign in to comment.