diff --git a/.changeset/tall-guests-arrive.md b/.changeset/tall-guests-arrive.md
new file mode 100644
index 0000000000..a9b79b95eb
--- /dev/null
+++ b/.changeset/tall-guests-arrive.md
@@ -0,0 +1,9 @@
+---
+"@digdir/designsystemet-css": patch
+"@digdir/designsystemet-react": patch
+---
+
+Chip:
+- Add `Chip.Button`
+- Rename `Chip.Toggle` to `Chip.Radio` and `Chip.Checkbox`
+- Remove `Chip.Group`
diff --git a/apps/theme/components/Previews/Components/Components.tsx b/apps/theme/components/Previews/Components/Components.tsx
index affa277bcd..60e81fb22c 100644
--- a/apps/theme/components/Previews/Components/Components.tsx
+++ b/apps/theme/components/Previews/Components/Components.tsx
@@ -275,11 +275,15 @@ export const Components = () => {
Filtrer på språk
diff --git a/packages/css/chip.css b/packages/css/chip.css
index d05a825f76..ab74432e6f 100644
--- a/packages/css/chip.css
+++ b/packages/css/chip.css
@@ -1,173 +1,134 @@
-/*
- The class is unused. There is no root chip component?
- .chip {
- display: flex;
- }
- */
-
-.ds-chip--button {
- --dsc-chip-height: var(--ds-sizing-7);
- --dsc-chip-padding: 0 var(--ds-spacing-3);
+.ds-chip {
--dsc-chip-background: var(--ds-color-accent-surface-default);
- --dsc-chip-text-color: var(--ds-color-accent-text-default);
- --dsc-chip-border: var(--ds-color-accent-border-subtle);
+ --dsc-chip-border-color: var(--ds-color-accent-border-subtle);
--dsc-chip-border-radius: var(--ds-border-radius-full);
+ --dsc-chip-color: var(--ds-color-accent-text-default);
+ --dsc-chip-height: var(--ds-sizing-8);
+ --dsc-chip-icon-size: var(--ds-spacing-6);
+ --dsc-chip-input-color: var(--ds-color-accent-border-default);
+ --dsc-chip-input-size: var(--ds-spacing-5);
+ --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);
- padding: var(--dsc-chip-padding);
- min-height: var(--dsc-chip-height);
border-radius: var(--dsc-chip-border-radius);
- border: 1px solid var(--dsc-chip-border);
- color: var(--dsc-chip-text-color);
- text-decoration: none;
- font-family: inherit;
+ border: 1px solid var(--dsc-chip-border-color);
+ box-sizing: border-box;
+ color: var(--dsc-chip-color);
+ cursor: pointer;
display: inline-flex;
- align-items: center;
-}
-
-.ds-chip--button:disabled,
-.ds-chip--button[aria-disabled='true'] {
- cursor: not-allowed;
- opacity: var(--ds-disabled-opacity);
-}
-
-.ds-chip--button .ds-chip__label {
- color: inherit;
- line-height: normal;
- display: flex;
- align-items: center;
- flex-direction: row;
- gap: var(--ds-spacing-2);
-}
-
-.ds-chip--removable {
- --dsc-removable-background: var(--ds-color-accent-base-default);
- --dsc-removable-text-color: var(--ds-color-neutral-contrast-default);
- --dsc-removable-chip-size: var(--ds-sizing-7);
- --dsc-removable-chip-xmark-color: var(--ds-color-neutral-contrast-default);
- --dsc-removable-chip-xmark-padding_right: var(--ds-spacing-1);
- --dsc-removable-chip-xmark-size: var(--ds-sizing-6);
- --dsc-removable-chip-xmark-wrapper-width: calc(var(--dsc-removable-chip-xmark-size) + var(--dsc-removable-chip-xmark-padding_right));
-
- color: var(--dsc-removable-text-color);
- background: var(--dsc-removable-background);
- border: 0;
- padding-right: var(--ds-spacing-2);
+ font-family: inherit;
min-height: var(--dsc-chip-height);
-}
-
-.ds-chip--removable.ds-chip--sm {
- padding-right: var(--ds-spacing-1);
-}
-
-.ds-chip--removable.ds-chip--lg {
- padding-right: var(--ds-spacing-2);
-}
-
-.ds-chip__x-mark {
- color: var(--dsc-removable-chip-xmark-color);
- height: var(--dsc-removable-chip-xmark-size);
- width: var(--dsc-removable-chip-xmark-size);
-}
-
-.ds-chip__x-mark .ds-chip__icon {
- height: var(--dsc-removable-chip-xmark-size);
- width: var(--dsc-removable-chip-xmark-size);
-}
-
-.ds-chip--spacing {
- padding-left: var(--ds-spacing-2) !important;
-}
-
-.ds-chip--sm .ds-chip__checkmark-icon {
- height: var(--ds-sizing-5);
- width: auto;
-}
-
-.ds-chip--md .ds-chip__checkmark-icon {
- height: 24px;
- width: auto;
-}
-
-.ds-chip--lg .ds-chip__checkmark-icon {
- height: 26px;
- width: auto;
-}
-
-.ds-chip--group-container {
- --dsc-chip-group-gap: var(--ds-spacing-2);
-
- align-items: center;
- display: flex;
- flex-wrap: wrap;
- gap: var(--dsc-chip-group-gap);
- list-style: none;
- margin: 0;
- padding: 0;
-}
+ padding: var(--dsc-chip-padding);
+ text-decoration: none;
-.ds-chip--group-container.ds-chip--sm {
- --dsc-chip-group-gap: var(--ds-spacing-2);
-}
+ /* Make focus ring also when input inside is focused */
+ &:has(:focus-visible) {
+ box-shadow: var(--dsc-focus-boxShadow);
+ }
-.ds-chip--group-container.ds-chip--md {
- --dsc-chip-group-gap: var(--ds-spacing-2);
-}
+ &:disabled,
+ &:has(input:disabled),
+ &[aria-disabled='true'] {
+ cursor: not-allowed;
+ opacity: var(--ds-disabled-opacity);
+ }
-.ds-chip--group-container.ds-chip--lg {
- --dsc-chip-group-gap: var(--ds-spacing-2);
-}
+ &:has(input[type='checkbox']) {
+ --dsc-chip-border-radius: var(--ds-border-radius-lg);
+ --dsc-chip-padding: 0 var(--ds-spacing-2) 0 var(--ds-spacing-3);
+ }
-/* Only use hover for non-touch devices to prevent sticky-hovering */
-@media (hover: hover) and (pointer: fine) {
- .ds-chip--button:not(:disabled, [aria-disabled='true']):hover {
- --dsc-chip-background: var(--ds-color-accent-surface-hover);
- --dsc-chip-text-color: var(--ds-color-accent-text-default);
- --dsc-chip-border: var(--ds-color-accent-border-default);
+ &[data-removable]::after {
+ --gap: calc((var(--dsc-chip-height) - var(--dsc-chip-icon-size)) / 2); /* Gap is based on remaining space between icon and edge */
- cursor: pointer;
+ background: currentcolor;
+ content: '';
+ height: var(--dsc-chip-icon-size);
+ margin: 0 calc(var(--gap) * -1) 0 var(--gap);
+ mask: center/contain no-repeat
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' fill='none' viewBox='0 0 24 24' focusable='false' role='img'%3E%3Cpath fill='currentColor' d='M6.53 5.47a.75.75 0 0 0-1.06 1.06L10.94 12l-5.47 5.47a.75.75 0 1 0 1.06 1.06L12 13.06l5.47 5.47a.75.75 0 1 0 1.06-1.06L13.06 12l5.47-5.47a.75.75 0 0 0-1.06-1.06L12 10.94z'%3E%3C/path%3E%3C/svg%3E");
+ width: var(--dsc-chip-icon-size);
}
- .ds-chip--button:not(:disabled, [aria-disabled='true']):is([aria-pressed='true']):hover {
- --dsc-chip-background: var(--ds-color-accent-surface-hover);
- --dsc-chip-text-color: var(--ds-color-accent-text-default);
+ &[data-size='sm'] {
+ --dsc-chip-height: var(--ds-sizing-7);
+ --dsc-chip-icon-size: var(--ds-sizing-5);
+ --dsc-chip-input-size: var(--ds-spacing-4);
}
- .ds-chip--removable:not(:disabled, [aria-disabled='true']):hover,
- .ds-chip--removable:not(:disabled, [aria-disabled='true']):focus {
- --dsc-removable-background: var(--ds-color-accent-base-hover);
- --dsc-removable-chip-xmark-color: var(--ds-color-accent-contrast-default);
+ &[data-size='lg'] {
+ --dsc-chip-height: var(--ds-sizing-9);
+ --dsc-chip-icon-size: var(--ds-sizing-7);
+ --dsc-chip-input-size: var(--ds-spacing-6);
}
-}
-
-.ds-chip--button:is([aria-pressed='true']),
-.ds-chip--button:not(:disabled, [aria-disabled='true']):active,
-.ds-chip--removable:is([aria-pressed='true']),
-.ds-chip--removable:not(:disabled, [aria-disabled='true']):active {
- --dsc-chip-background: var(--ds-color-accent-base-active);
- --dsc-chip-text-color: var(--ds-color-accent-contrast-default);
- --dsc-chip-border: var(--ds-color-accent-base-active);
- --dsc-removable-background: var(--ds-color-accent-base-active);
- --dsc-removable-text-color: var(--ds-color-neutral-contrast-default);
-}
-.ds-chip--sm {
- --dsc-chip-height: var(--ds-sizing-7);
- --dsc-chip-padding: 0 var(--ds-spacing-3);
- --dsc-removable-chip-xmark-size: var(--ds-sizing-5);
- --dsc-removable-chip-xmark-padding_right: var(--ds-spacing-1);
-}
+ & > input {
+ --gap: calc((var(--dsc-chip-height) - var(--dsc-chip-input-size)) / 2); /* Gap is based on remaining space between input and edge */
+
+ appearance: none;
+ background: none;
+ border-radius: calc(var(--dsc-chip-border-radius) - (var(--gap) / 2)); /* Use same radius as */
+ border: 2px solid;
+ box-sizing: border-box;
+ color: var(--dsc-chip-input-color);
+ height: var(--dsc-chip-input-size);
+ margin: 0 var(--gap) 0 calc(var(--gap) * -1);
+ outline: none;
+ width: var(--dsc-chip-input-size);
+
+ &[type='radio']:checked {
+ border-width: calc(var(--dsc-chip-input-size) / 3.5); /* Matches Figma */
+ }
+
+ &[type='checkbox']:checked {
+ background: currentcolor;
+ mask: center/cover no-repeat
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M24 0H0v24h24V0Zm-4.44 8.56a1.5 1.5 0 0 0-2.12-2.12L10 13.88l-3.44-3.44a1.5 1.5 0 0 0-2.12 2.12l4.5 4.5a1.5 1.5 0 0 0 2.12 0l8.5-8.5Z'/%3E%3C/svg%3E");
+ }
+ }
-.ds-chip--md {
- --dsc-chip-height: var(--ds-sizing-8);
- --dsc-chip-padding: 0 var(--ds-spacing-3);
- --dsc-removable-chip-xmark-size: var(--ds-sizing-6);
- --dsc-removable-chip-xmark-padding_right: var(--ds-spacing-2);
-}
+ &:has(input:checked),
+ &[data-removable] {
+ --dsc-chip-background: var(--ds-color-accent-base-default);
+ --dsc-chip-border-color: transparent;
+ --dsc-chip-color: var(--ds-color-accent-contrast-default);
+ --dsc-chip-input-color: currentcolor;
+ }
-.ds-chip--lg {
- --dsc-chip-height: var(--ds-sizing-9);
- --dsc-chip-padding: 0 var(--ds-spacing-4);
- --dsc-removable-chip-xmark-size: var(--ds-sizing-7);
- --dsc-removable-chip-xmark-padding_right: var(--ds-spacing-3);
+ /* Only use hover for non-touch devices to prevent sticky-hovering */
+ @media (hover: hover) and (pointer: fine) {
+ &:where(:not(:disabled, :has(input:disabled), [aria-disabled='true'])) {
+ &:hover {
+ --dsc-chip-background: var(--ds-color-accent-surface-hover);
+ --dsc-chip-border-color: var(--ds-color-accent-border-default);
+ --dsc-chip-color: var(--ds-color-accent-text-default);
+ --dsc-chip-input-color: var(--ds-color-accent-border-strong);
+ }
+
+ &:active {
+ --dsc-chip-background: var(--ds-color-accent-surface-active);
+ --dsc-chip-border-color: var(--ds-color-accent-border-strong);
+ --dsc-chip-input-color: var(--ds-color-accent-border-strong);
+ }
+
+ &[data-removable]:hover,
+ &:has(input:checked):hover {
+ --dsc-chip-background: var(--ds-color-accent-base-hover);
+ --dsc-chip-border-color: transparent;
+ --dsc-chip-color: var(--ds-color-accent-contrast-default);
+ --dsc-chip-input-color: currentcolor;
+ }
+
+ &[data-removable]:active,
+ &:has(input:checked):active {
+ --dsc-chip-background: var(--ds-color-accent-base-active);
+ --dsc-chip-border-color: transparent;
+ --dsc-chip-color: var(--ds-color-accent-contrast-default);
+ }
+ }
+ }
}
diff --git a/packages/react/src/components/Chip/Chip.mdx b/packages/react/src/components/Chip/Chip.mdx
index 2cdd662e49..b355320f86 100644
--- a/packages/react/src/components/Chip/Chip.mdx
+++ b/packages/react/src/components/Chip/Chip.mdx
@@ -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';
@@ -23,33 +20,28 @@ import * as GroupChipStories from './Group/Group.stories';
```tsx
import { Chip } from '@digdir/designsystemet-react';
-
You are using the Chip component!;
-```
-
-
-
-## Eksempler
-
-
-### `Chip.Toggle`
-`Chip.Toggle` kan brukes som et alternativ til `Button`. Den brukes når handlingen(e) er direkte relatert til hovedinnholdet.
-
-
+// som radio
+
You are using the Chip component!;
+
You are using the Chip component!;
-### `Chip.Group`
+// som checkbox
+
You are using the Chip component!;
-Denne kodeblokken viser hvordan du grupperer `Chip` ved hjelp av `Chip.Group`.
+// som removable
+
You are using the Chip component!;
-
-
+// som button
+
You are using the Chip component!;
+```
-### `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.
-
-
+## Eksempler
+
+### `Chip.Checkbox`
+`Chip.Checkbox` kan brukes som et alternativ til `Checkbox`.
+
### `Chip.Removable`
@@ -58,12 +50,12 @@ 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}"`.
-
-
+
+
+### `Chip.Button`
+`Chip.Button` kan brukes i gruppe, for å vise valgmuligheter - ikke handlinger.
+
-
-
-
## Retningslinjer for `Chip`
@@ -89,4 +81,4 @@ Chips bør ha så få ord som mulig, helst bare ett eller to.
## 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.
\ No newline at end of file
+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.
diff --git a/packages/react/src/components/Chip/Chip.stories.tsx b/packages/react/src/components/Chip/Chip.stories.tsx
index b4753a1271..5742506c37 100644
--- a/packages/react/src/components/Chip/Chip.stories.tsx
+++ b/packages/react/src/components/Chip/Chip.stories.tsx
@@ -1,25 +1,46 @@
-import type { Meta, StoryObj } from '@storybook/react';
+import type { Meta, StoryFn, StoryObj } from '@storybook/react';
import { Chip } from '.';
-type Story = StoryObj
;
-
export default {
title: 'Komponenter/Chip',
- component: Chip.Toggle,
- argTypes: {
- size: {
- options: ['sm', 'md', 'lg'],
- control: { type: 'radio' },
- },
- },
+ component: Chip.Radio,
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
} as Meta;
-export const Preview: Story = {
- args: {
- children: 'Nynorsk',
- size: 'md',
- selected: false,
- checkmark: false,
- },
+export const Preview: StoryFn = (args) => (
+ <>
+
+ Nynorsk
+
+
+ Bokmål
+
+ >
+);
+
+export const Checkbox: StoryFn = (args) => (
+ Nynorsk
+);
+
+export const Removable: StoryFn = (args) => (
+ Norge
+);
+
+Removable.args = {
+ 'aria-label': 'Slett Norge',
};
+
+export const Button: StoryFn = (args) => (
+ <>
+ Søk etter nynorsk
+ Søk etter bokmål
+ Søk etter engelsk
+ >
+);
diff --git a/packages/react/src/components/Chip/Chips.test.tsx b/packages/react/src/components/Chip/Chips.test.tsx
new file mode 100644
index 0000000000..70ba6d2382
--- /dev/null
+++ b/packages/react/src/components/Chip/Chips.test.tsx
@@ -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(Norwegian);
+ const chip = screen.getByRole('button', { name: 'Norwegian' });
+
+ expect(chip).toBeInTheDocument();
+ });
+
+ it('rest props should be supported', () => {
+ render(Norwegian);
+
+ 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(Norwegian);
+
+ const chip = screen.getByRole('button', { name: 'Norwegian' });
+ expect(chip).toBeInTheDocument();
+ expect(chip).toHaveAttribute('data-removable');
+ });
+
+ it('rest props should be supported', () => {
+ render(Norwegian);
+
+ 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(Norwegian);
+
+ const chip = screen.getByLabelText('Norwegian');
+ const input = screen.getByRole('checkbox');
+ expect(chip).toBeInTheDocument();
+ expect(input).toBeInTheDocument();
+ });
+
+ it('rest props should be supported', () => {
+ render(
+
+ Norwegian
+ ,
+ );
+
+ 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(Norwegian);
+
+ const chip = screen.getByLabelText('Norwegian');
+ const input = screen.getByRole('radio');
+ expect(chip).toBeInTheDocument();
+ expect(input).toBeInTheDocument();
+ });
+
+ it('rest props should be supported', () => {
+ render(
+
+ Norwegian
+ ,
+ );
+
+ 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');
+ });
+});
diff --git a/packages/react/src/components/Chip/Chips.tsx b/packages/react/src/components/Chip/Chips.tsx
new file mode 100644
index 0000000000..8f633f852d
--- /dev/null
+++ b/packages/react/src/components/Chip/Chips.tsx
@@ -0,0 +1,92 @@
+import { Slot, Slottable } from '@radix-ui/react-slot';
+import cl from 'clsx/lite';
+import { forwardRef } from 'react';
+import type { ButtonHTMLAttributes, InputHTMLAttributes } from 'react';
+
+type ChipBaseProps = {
+ /**
+ * Size
+ * @default md
+ */
+ size?: 'sm' | 'md' | 'lg';
+ /**
+ * Change the default rendered element for the one passed as a child, merging their props and behavior.
+ * @default false
+ */
+ asChild?: boolean;
+};
+
+export type ChipRemovableProps = ChipButtonProps;
+export type ChipRadioProps = ChipCheckboxProps;
+export type ChipButtonProps = ChipBaseProps &
+ ButtonHTMLAttributes;
+export type ChipCheckboxProps = ChipBaseProps &
+ Omit, 'type' | 'size'>;
+
+/**
+ * Chip.Button used for interaction
+ * @example
+ * Click me
+ */
+export const ChipButton = forwardRef(
+ function ChipButton({ asChild, className, size, ...rest }, ref) {
+ const Component = asChild ? Slot : 'button';
+
+ return (
+
+ );
+ },
+);
+
+/**
+ * Chip.Removable used for interaction
+ * @example
+ * Click to remove me
+ */
+export const ChipRemovable = forwardRef(
+ function ChipRemovable(props, ref) {
+ return ;
+ },
+);
+
+/**
+ * Chip.Checkbox used for multiselection
+ * @example
+ * Nynorsk
+ * Bokmål
+ */
+export const ChipCheckbox = forwardRef(
+ function ChipCheckbox({ asChild, children, className, size, ...rest }, ref) {
+ const inputType = (rest as { type?: string }).type ?? 'checkbox';
+ const Component = asChild ? Slot : 'label';
+
+ return (
+
+
+ {children}
+
+ );
+ },
+);
+
+/**
+ * Chip.Radio used for single selection
+ * @example
+ * Nynorsk
+ * Bokmål
+ */
+export const ChipRadio = forwardRef(
+ function ChipRadio(props, ref) {
+ return ;
+ },
+);
diff --git a/packages/react/src/components/Chip/Group/Group.stories.tsx b/packages/react/src/components/Chip/Group/Group.stories.tsx
deleted file mode 100644
index d5f70b14aa..0000000000
--- a/packages/react/src/components/Chip/Group/Group.stories.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import type { Meta, StoryFn } from '@storybook/react';
-
-import { Chip } from '..';
-
-import { Group } from './Group';
-
-const meta: Meta = {
- title: 'Komponenter/Chip/Group',
- component: Group,
- argTypes: {
- size: {
- options: ['sm', 'md', 'lg'],
- control: { type: 'radio' },
- },
- },
-};
-
-export default meta;
-
-type Story = StoryFn;
-
-export const Preview: Story = (args) => (
-
- Nynorsk
- Bokmål
-
-);
-
-Preview.args = {
- size: 'sm',
-};
-
-export const CheckGroup: Story = (args) => (
-
-
- Utsikt
-
- Heis
-
- Strandlinje
-
- Vaskemaskin
- Dyrevennlig
-
-);
-
-export const RemoveGroup: Story = (args) => (
-
- Norge
- Danmark
- Sverige
- Finland
-
-);
diff --git a/packages/react/src/components/Chip/Group/Group.test.tsx b/packages/react/src/components/Chip/Group/Group.test.tsx
deleted file mode 100644
index f62f3342bf..0000000000
--- a/packages/react/src/components/Chip/Group/Group.test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { render, screen } from '@testing-library/react';
-
-import { Chip } from '..';
-
-describe('Chip.Group', () => {
- it('should be possible to render a group', () => {
- render(
-
- First item
- Second item
- Third item
- ,
- );
-
- const [fistChip, secondChip, thirdChip] = screen.getAllByRole('listitem');
- expect(screen.getByRole('list'));
- expect(fistChip).toHaveTextContent('First item');
- expect(secondChip).toHaveTextContent('Second item');
- expect(thirdChip).toHaveTextContent('Third item');
- });
-});
diff --git a/packages/react/src/components/Chip/Group/Group.tsx b/packages/react/src/components/Chip/Group/Group.tsx
deleted file mode 100644
index 90e609d909..0000000000
--- a/packages/react/src/components/Chip/Group/Group.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import cl from 'clsx/lite';
-import type { HTMLAttributes } from 'react';
-import { Children, createContext, forwardRef, isValidElement } from 'react';
-
-export type ChipGroupContext = {
- size?: 'sm' | 'md' | 'lg';
-};
-
-export const ChipGroupContext = createContext(null);
-
-export type ChipGroupProps = {
- /**
- * Changes Chip size and gap between chips.
- * @default md
- */
- size?: ChipGroupContext['size'];
-} & HTMLAttributes;
-
-export const Group = forwardRef(
- ({ size = 'md', children, className, ...rest }: ChipGroupProps, ref) => {
- return (
-
-
- {Children.toArray(children).map((child, index) =>
- isValidElement(child) ? (
- - {child}
- ) : null,
- )}
-
-
- );
- },
-);
-
-Group.displayName = 'ChipGroup';
diff --git a/packages/react/src/components/Chip/Removable/Removable.stories.tsx b/packages/react/src/components/Chip/Removable/Removable.stories.tsx
deleted file mode 100644
index 302c8802fd..0000000000
--- a/packages/react/src/components/Chip/Removable/Removable.stories.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react';
-
-import { RemovableChip } from './Removable';
-
-export default {
- title: 'Komponenter/Chip/Removable',
- component: RemovableChip,
- argTypes: {
- size: {
- options: ['sm', 'md', 'lg'],
- control: { type: 'radio' },
- },
- },
-} as Meta;
-
-type Story = StoryObj;
-
-export const Preview: Story = {
- args: {
- children: 'Nynorsk',
- size: 'md',
- 'aria-label': 'Slett nynorsk',
- disabled: false,
- },
-};
diff --git a/packages/react/src/components/Chip/Removable/Removable.test.tsx b/packages/react/src/components/Chip/Removable/Removable.test.tsx
deleted file mode 100644
index a9e157346c..0000000000
--- a/packages/react/src/components/Chip/Removable/Removable.test.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { act, useState } from 'react';
-
-import { Chip, type RemovableChipProps } from '..';
-
-const user = userEvent.setup();
-
-const TestComponent = ({
- children,
- ...rest
-}: RemovableChipProps): JSX.Element => {
- const [removed, setRemoved] = useState(false);
-
- return (
- <>
- {!removed && (
- setRemoved(true)}>
- {children}
-
- )}
- >
- );
-};
-
-describe('RemovableChip', () => {
- it('should render button as native element', () => {
- render(Norwegian);
-
- expect(screen.getByRole('button', { name: 'Norwegian' }));
- });
-
- it('should be removed based on user interaction', async () => {
- render(Norwegian);
- const chip = screen.getByRole('button', { name: 'Norwegian' });
-
- expect(chip);
- await act(async () => await user.click(chip));
- expect(
- screen.queryByRole('button', { name: 'Norwegian' }),
- ).not.toBeInTheDocument();
- });
-
- it('rest props should be supported', () => {
- render(Norwegian);
-
- 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');
- });
-});
diff --git a/packages/react/src/components/Chip/Removable/Removable.tsx b/packages/react/src/components/Chip/Removable/Removable.tsx
deleted file mode 100644
index ea67f3f0ed..0000000000
--- a/packages/react/src/components/Chip/Removable/Removable.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { XMarkIcon } from '@navikt/aksel-icons';
-import cl from 'clsx/lite';
-import type { ButtonHTMLAttributes } from 'react';
-import { forwardRef, useContext } from 'react';
-
-import { Paragraph } from '../../Typography';
-import { ChipGroupContext } from '../Group/Group';
-
-export type RemovableChipProps = {
- /**
- * Changes Chip size and gap between chips.
- * @default 'md'
- */
- size?: ChipGroupContext['size'];
-} & ButtonHTMLAttributes;
-
-export const RemovableChip = forwardRef(
- ({ size = 'md', children, className, ...rest }, ref) => {
- const group = useContext(ChipGroupContext);
-
- return (
-
- );
- },
-);
-
-RemovableChip.displayName = 'ChipRemovable';
diff --git a/packages/react/src/components/Chip/Toggle/Toggle.stories.tsx b/packages/react/src/components/Chip/Toggle/Toggle.stories.tsx
deleted file mode 100644
index 715aa3f15e..0000000000
--- a/packages/react/src/components/Chip/Toggle/Toggle.stories.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react';
-
-import { ToggleChip } from './Toggle';
-
-const meta: Meta = {
- title: 'Komponenter/Chip/Toggle',
- component: ToggleChip,
- argTypes: {
- size: {
- options: ['sm', 'md', 'lg'],
- control: { type: 'radio' },
- },
- },
-};
-
-export default meta;
-
-type Story = StoryObj;
-
-export const Preview: Story = {
- args: {
- children: 'Nynorsk',
- size: 'md',
- selected: false,
- checkmark: false,
- disabled: false,
- },
-};
diff --git a/packages/react/src/components/Chip/Toggle/Toggle.test.tsx b/packages/react/src/components/Chip/Toggle/Toggle.test.tsx
deleted file mode 100644
index 5edfe7d2ff..0000000000
--- a/packages/react/src/components/Chip/Toggle/Toggle.test.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { act, useState } from 'react';
-
-import { Chip, type ToggleChipProps } from '..';
-
-const user = userEvent.setup();
-
-const TestComponent = ({ children, ...rest }: ToggleChipProps): JSX.Element => {
- const [selected, setSelected] = useState(false);
-
- return (
- setSelected(!selected)}
- >
- {children}
-
- );
-};
-
-describe('ToggleChip', () => {
- it('should render button as native element', () => {
- render(Norwegian);
-
- expect(screen.getByRole('button', { name: 'Norwegian' }));
- });
-
- it('should be possible to render as selected', () => {
- render(Norwegian);
-
- expect(screen.getByRole('button', { name: 'Norwegian' })).toHaveAttribute(
- 'aria-pressed',
- 'true',
- );
- });
-
- it('should toggle aria-pressed based on user interaction', async () => {
- render(Norwegian);
- const chip = screen.getByRole('button', { name: 'Norwegian' });
-
- expect(chip).toHaveAttribute('aria-pressed', 'false');
- await act(async () => await user.click(chip));
- expect(chip).toHaveAttribute('aria-pressed', 'true');
- });
-
- it('rest props should be supported', () => {
- render(Norwegian);
-
- 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');
- });
-});
diff --git a/packages/react/src/components/Chip/Toggle/Toggle.tsx b/packages/react/src/components/Chip/Toggle/Toggle.tsx
deleted file mode 100644
index 7232fed9e9..0000000000
--- a/packages/react/src/components/Chip/Toggle/Toggle.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { CheckmarkIcon } from '@navikt/aksel-icons';
-import cl from 'clsx/lite';
-import type { ButtonHTMLAttributes } from 'react';
-import { forwardRef, useContext } from 'react';
-
-import { Paragraph } from '../../Typography';
-import { ChipGroupContext } from '../Group/Group';
-
-export type ToggleChipProps = {
- /**
- * Enables check mark icon
- */
- checkmark?: boolean;
- /**
- * Changes Chip size and gap between chips.
- * @default 'md'
- */
- size?: ChipGroupContext['size'];
- /**
- * Toggles `aria-pressed` and visual-changes
- * */
- selected?: boolean;
-} & ButtonHTMLAttributes;
-
-export const ToggleChip = forwardRef(
- (
- {
- children,
- selected = false,
- checkmark = true,
- size = 'md',
- className,
- ...rest
- }: ToggleChipProps,
- ref,
- ) => {
- const shouldDisplayCheckmark = checkmark && selected;
- const group = useContext(ChipGroupContext);
-
- return (
-
- );
- },
-);
-
-ToggleChip.displayName = 'ChipToggle';
diff --git a/packages/react/src/components/Chip/index.ts b/packages/react/src/components/Chip/index.ts
index 604a04858c..694b62f2ec 100644
--- a/packages/react/src/components/Chip/index.ts
+++ b/packages/react/src/components/Chip/index.ts
@@ -1,33 +1,21 @@
-import { Group as ChipGroup } from './Group/Group';
-import type { ChipGroupProps } from './Group/Group';
-import { RemovableChip as ChipRemovable } from './Removable/Removable';
-import type { RemovableChipProps } from './Removable/Removable';
-import { ToggleChip as ChipToggle } from './Toggle/Toggle';
-import type { ToggleChipProps } from './Toggle/Toggle';
+import { ChipButton, ChipCheckbox, ChipRadio, ChipRemovable } from './Chips';
-type ChipComponent = {
- /**
- * Grouping multiple `Chip` together. Avoid mixing different kind of chips.
- * @example
- *
- * Tekst
- * Tekst
- *
- */
- Group: typeof ChipGroup;
- Removable: typeof ChipRemovable;
- Toggle: typeof ChipToggle;
-};
-
-const Chip: ChipComponent = {
- Group: ChipGroup,
+const Chip = {
+ Button: ChipButton,
+ Checkbox: ChipCheckbox,
+ Radio: ChipRadio,
Removable: ChipRemovable,
- Toggle: ChipToggle,
};
-Chip.Group.displayName = 'Chip.Group';
+Chip.Button.displayName = 'Chip.Button';
+Chip.Checkbox.displayName = 'Chip.Checkbox';
+Chip.Radio.displayName = 'Chip.Radio';
Chip.Removable.displayName = 'Chip.Removable';
-Chip.Toggle.displayName = 'Chip.Toggle';
-export type { RemovableChipProps, ToggleChipProps, ChipGroupProps };
-export { Chip, ChipGroup, ChipRemovable, ChipToggle };
+export type {
+ ChipButtonProps,
+ ChipCheckboxProps,
+ ChipRadioProps,
+ ChipRemovableProps,
+} from './Chips';
+export { Chip, ChipButton, ChipCheckbox, ChipRadio, ChipRemovable };
diff --git a/packages/react/src/components/form/Combobox/Combobox.stories.tsx b/packages/react/src/components/form/Combobox/Combobox.stories.tsx
index 20ca6389f3..171beb53dc 100644
--- a/packages/react/src/components/form/Combobox/Combobox.stories.tsx
+++ b/packages/react/src/components/form/Combobox/Combobox.stories.tsx
@@ -3,7 +3,7 @@ import { useRef, useState } from 'react';
import type { FormEvent } from 'react';
import { Button } from '../../Button';
-import { ChipRemovable } from '../../Chip';
+import { Chip } from '../../Chip';
import { Modal } from '../../Modal';
import { Heading, Paragraph } from '../../Typography';
import { Switch } from '../Switch';
@@ -316,14 +316,14 @@ export const WithChipsOutside: StoryFn = (args) => {
}}
>
{value.map((item, index) => (
- {
setValue(value.filter((v) => v !== item));
}}
>
{item}
-
+
))}
diff --git a/packages/react/stories/testing.stories.tsx b/packages/react/stories/testing.stories.tsx
index 20ac36b5e5..016eb3083b 100644
--- a/packages/react/stories/testing.stories.tsx
+++ b/packages/react/stories/testing.stories.tsx
@@ -83,7 +83,7 @@ export const MediumRow: StoryFn<{
>