aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/lists
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-10-02 17:01:57 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:14:41 +0100
commit36890cfafeba6e30782df1260d7f9e678c7da4bf (patch)
tree1abe20cf36d60e048b75828dd5516529e504ddd8 /src/components/atoms/lists
parent4f768afe543bbf9e1857c41d03804f8e37ab3512 (diff)
refactor(components): rewrite DescriptionList component
* add a `spacing` prop * replace `layout` prop with `isInline` prop * remove `items` prop (and classNames props) in favor of new components: Description, Group, Term * remove `withSeparator` prop (CSS content is announced by screen readers and Firefox/Safari have no support for alternative text so the consumer should add itself an element with `aria-hidden` if it need a separator) Be aware, Meta component and its consumers can be visually broken, they should be refactored before using them in production.
Diffstat (limited to 'src/components/atoms/lists')
-rw-r--r--src/components/atoms/lists/description-list-group.module.scss40
-rw-r--r--src/components/atoms/lists/description-list-group.stories.tsx132
-rw-r--r--src/components/atoms/lists/description-list-group.test.tsx18
-rw-r--r--src/components/atoms/lists/description-list-group.tsx70
-rw-r--r--src/components/atoms/lists/description-list.module.scss15
-rw-r--r--src/components/atoms/lists/description-list.stories.tsx131
-rw-r--r--src/components/atoms/lists/description-list.test.tsx21
-rw-r--r--src/components/atoms/lists/description-list.tsx103
-rw-r--r--src/components/atoms/lists/description-list/description-list.module.scss28
-rw-r--r--src/components/atoms/lists/description-list/description-list.stories.tsx150
-rw-r--r--src/components/atoms/lists/description-list/description-list.test.tsx70
-rw-r--r--src/components/atoms/lists/description-list/description-list.tsx67
-rw-r--r--src/components/atoms/lists/description-list/description.tsx28
-rw-r--r--src/components/atoms/lists/description-list/group.tsx62
-rw-r--r--src/components/atoms/lists/description-list/index.ts4
-rw-r--r--src/components/atoms/lists/description-list/term.tsx28
-rw-r--r--src/components/atoms/lists/index.ts1
17 files changed, 437 insertions, 531 deletions
diff --git a/src/components/atoms/lists/description-list-group.module.scss b/src/components/atoms/lists/description-list-group.module.scss
deleted file mode 100644
index aba90ce..0000000
--- a/src/components/atoms/lists/description-list-group.module.scss
+++ /dev/null
@@ -1,40 +0,0 @@
-.term {
- color: var(--color-fg-light);
- font-weight: 600;
-}
-
-.description {
- margin: 0;
- word-break: break-all;
-}
-
-.wrapper {
- display: flex;
- width: fit-content;
-
- &--has-separator {
- .description:not(:first-of-type) {
- &::before {
- content: "/\0000a0";
- }
- }
- }
-
- &--inline,
- &--inline-values {
- flex-flow: row wrap;
- column-gap: var(--spacing-2xs);
- }
-
- &--inline-values {
- row-gap: var(--spacing-2xs);
-
- .term {
- flex: 1 1 100%;
- }
- }
-
- &--stacked {
- flex-flow: column wrap;
- }
-}
diff --git a/src/components/atoms/lists/description-list-group.stories.tsx b/src/components/atoms/lists/description-list-group.stories.tsx
deleted file mode 100644
index e6766a3..0000000
--- a/src/components/atoms/lists/description-list-group.stories.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { DescriptionListGroup } from './description-list-group';
-
-export default {
- title: 'Atoms/Typography/Lists/DescriptionList/Item',
- component: DescriptionListGroup,
- args: {
- layout: 'stacked',
- withSeparator: false,
- },
- argTypes: {
- className: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the list item wrapper.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- descriptionClassName: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the list item description.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- label: {
- control: {
- type: 'text',
- },
- description: 'The item label.',
- type: {
- name: 'string',
- required: true,
- },
- },
- layout: {
- control: {
- type: 'select',
- },
- description: 'The item layout.',
- options: ['inline', 'inline-values', 'stacked'],
- table: {
- category: 'Options',
- defaultValue: { summary: 'stacked' },
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- termClassName: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the list item term.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- value: {
- description: 'The item value.',
- type: {
- name: 'object',
- required: true,
- value: {},
- },
- },
- withSeparator: {
- control: {
- type: 'boolean',
- },
- description: 'Add a slash as separator between multiple values.',
- table: {
- category: 'Options',
- defaultValue: { summary: false },
- },
- type: {
- name: 'boolean',
- required: false,
- },
- },
- },
-} as ComponentMeta<typeof DescriptionListGroup>;
-
-const Template: ComponentStory<typeof DescriptionListGroup> = (args) => (
- <DescriptionListGroup {...args} />
-);
-
-export const SingleValueStacked = Template.bind({});
-SingleValueStacked.args = {
- label: 'Recusandae vitae tenetur',
- value: ['praesentium'],
- layout: 'stacked',
-};
-
-export const SingleValueInlined = Template.bind({});
-SingleValueInlined.args = {
- label: 'Recusandae vitae tenetur',
- value: ['praesentium'],
- layout: 'inline',
-};
-
-export const MultipleValuesStacked = Template.bind({});
-MultipleValuesStacked.args = {
- label: 'Recusandae vitae tenetur',
- value: ['praesentium', 'voluptate', 'tempore'],
- layout: 'stacked',
-};
-
-export const MultipleValuesInlined = Template.bind({});
-MultipleValuesInlined.args = {
- label: 'Recusandae vitae tenetur',
- value: ['praesentium', 'voluptate', 'tempore'],
- layout: 'inline-values',
- withSeparator: true,
-};
diff --git a/src/components/atoms/lists/description-list-group.test.tsx b/src/components/atoms/lists/description-list-group.test.tsx
deleted file mode 100644
index 205dad5..0000000
--- a/src/components/atoms/lists/description-list-group.test.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../tests/utils';
-import { DescriptionListGroup } from './description-list-group';
-
-const itemLabel = 'Repellendus corporis facilis';
-const itemValue = ['quos', 'eum'];
-
-describe('DescriptionListGroup', () => {
- it('renders a couple of label', () => {
- render(<DescriptionListGroup label={itemLabel} value={itemValue} />);
- expect(screen.getByRole('term')).toHaveTextContent(itemLabel);
- });
-
- it('renders the right number of values', () => {
- render(<DescriptionListGroup label={itemLabel} value={itemValue} />);
- expect(screen.getAllByRole('definition')).toHaveLength(itemValue.length);
- });
-});
diff --git a/src/components/atoms/lists/description-list-group.tsx b/src/components/atoms/lists/description-list-group.tsx
deleted file mode 100644
index 63ae541..0000000
--- a/src/components/atoms/lists/description-list-group.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { FC, ReactNode, useId } from 'react';
-import styles from './description-list-group.module.scss';
-
-export type ItemLayout = 'inline' | 'inline-values' | 'stacked';
-
-export type DescriptionListGroupProps = {
- /**
- * Set additional classnames to the list item wrapper.
- */
- className?: string;
- /**
- * Set additional classnames to the list item description.
- */
- descriptionClassName?: string;
- /**
- * The item label.
- */
- label: string;
- /**
- * The item layout.
- */
- layout?: ItemLayout;
- /**
- * Set additional classnames to the list item term.
- */
- termClassName?: string;
- /**
- * The item value.
- */
- value: ReactNode | ReactNode[];
- /**
- * If true, use a slash to delimitate multiple values.
- */
- withSeparator?: boolean;
-};
-
-/**
- * DescriptionListItem component
- *
- * Render a couple of dt/dd wrapped in a div.
- */
-export const DescriptionListGroup: FC<DescriptionListGroupProps> = ({
- className = '',
- descriptionClassName = '',
- label,
- termClassName = '',
- value,
- layout = 'stacked',
- withSeparator = false,
-}) => {
- const id = useId();
- const layoutStyles = styles[`wrapper--${layout}`];
- const separatorStyles = withSeparator ? styles['wrapper--has-separator'] : '';
- const itemValues = Array.isArray(value) ? value : [value];
- const groupClass = `${styles.wrapper} ${layoutStyles} ${separatorStyles} ${className}`;
-
- return (
- <div className={groupClass}>
- <dt className={`${styles.term} ${termClassName}`}>{label}</dt>
- {itemValues.map((currentValue, index) => (
- <dd
- className={`${styles.description} ${descriptionClassName}`}
- key={`${id}-${index}`}
- >
- {currentValue}
- </dd>
- ))}
- </div>
- );
-};
diff --git a/src/components/atoms/lists/description-list.module.scss b/src/components/atoms/lists/description-list.module.scss
deleted file mode 100644
index d31c88a..0000000
--- a/src/components/atoms/lists/description-list.module.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-.list {
- display: flex;
- column-gap: var(--spacing-md);
- row-gap: var(--spacing-2xs);
- margin: 0;
-
- &--inline {
- flex-flow: row wrap;
- align-items: baseline;
- }
-
- &--column {
- flex-flow: column wrap;
- }
-}
diff --git a/src/components/atoms/lists/description-list.stories.tsx b/src/components/atoms/lists/description-list.stories.tsx
deleted file mode 100644
index 0194817..0000000
--- a/src/components/atoms/lists/description-list.stories.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { DescriptionList, DescriptionListItem } from './description-list';
-
-/**
- * DescriptionList - Storybook Meta
- */
-export default {
- title: 'Atoms/Typography/Lists/DescriptionList',
- component: DescriptionList,
- args: {
- layout: 'column',
- withSeparator: false,
- },
- argTypes: {
- className: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the list wrapper',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- groupClassName: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the item wrapper.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- items: {
- control: {
- type: null,
- },
- description: 'The list items.',
- type: {
- name: 'object',
- required: true,
- value: {},
- },
- },
- labelClassName: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the label wrapper.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- layout: {
- control: {
- type: 'select',
- },
- description: 'The list layout.',
- options: ['column', 'inline'],
- table: {
- category: 'Options',
- defaultValue: { summary: 'column' },
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- valueClassName: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the value wrapper.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- withSeparator: {
- control: {
- type: 'boolean',
- },
- description: 'Add a slash as separator between multiple values.',
- table: {
- category: 'Options',
- defaultValue: { summary: false },
- },
- type: {
- name: 'boolean',
- required: false,
- },
- },
- },
-} as ComponentMeta<typeof DescriptionList>;
-
-const Template: ComponentStory<typeof DescriptionList> = (args) => (
- <DescriptionList {...args} />
-);
-
-const items: DescriptionListItem[] = [
- { id: 'term-1', label: 'Term 1:', value: ['Value for term 1'] },
- { id: 'term-2', label: 'Term 2:', value: ['Value for term 2'] },
- {
- id: 'term-3',
- label: 'Term 3:',
- value: ['Value 1 for term 3', 'Value 2 for term 3', 'Value 3 for term 3'],
- },
- { id: 'term-4', label: 'Term 4:', value: ['Value for term 4'] },
-];
-
-/**
- * List Stories - Description list
- */
-export const List = Template.bind({});
-List.args = {
- items,
-};
diff --git a/src/components/atoms/lists/description-list.test.tsx b/src/components/atoms/lists/description-list.test.tsx
deleted file mode 100644
index 2af92e2..0000000
--- a/src/components/atoms/lists/description-list.test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { describe, expect, it } from '@jest/globals';
-import { render } from '../../../../tests/utils';
-import { DescriptionList, DescriptionListItem } from './description-list';
-
-const items: DescriptionListItem[] = [
- { id: 'term-1', label: 'Term 1:', value: ['Value for term 1'] },
- { id: 'term-2', label: 'Term 2:', value: ['Value for term 2'] },
- {
- id: 'term-3',
- label: 'Term 3:',
- value: ['Value 1 for term 3', 'Value 2 for term 3', 'Value 3 for term 3'],
- },
- { id: 'term-4', label: 'Term 4:', value: ['Value for term 4'] },
-];
-
-describe('DescriptionList', () => {
- it('renders a list of terms and description', () => {
- const { container } = render(<DescriptionList items={items} />);
- expect(container).toBeDefined();
- });
-});
diff --git a/src/components/atoms/lists/description-list.tsx b/src/components/atoms/lists/description-list.tsx
deleted file mode 100644
index d97e505..0000000
--- a/src/components/atoms/lists/description-list.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { FC, HTMLAttributes } from 'react';
-import {
- DescriptionListGroup,
- type DescriptionListGroupProps,
-} from './description-list-group';
-import styles from './description-list.module.scss';
-
-export type DescriptionListItem = {
- /**
- * The item id.
- */
- id: string;
- /**
- * The list item layout.
- */
- layout?: DescriptionListGroupProps['layout'];
- /**
- * A list label.
- */
- label: DescriptionListGroupProps['label'];
- /**
- * An array of values for the list item.
- */
- value: DescriptionListGroupProps['value'];
-};
-
-export type DescriptionListProps = Omit<
- HTMLAttributes<HTMLDListElement>,
- 'children'
-> & {
- /**
- * Set additional classnames to the `dt`/`dd` couple wrapper.
- */
- groupClassName?: string;
- /**
- * The list items.
- */
- items: DescriptionListItem[];
- /**
- * Set additional classnames to the `dt` element.
- */
- labelClassName?: string;
- /**
- * The list layout. Default: column.
- */
- layout?: 'inline' | 'column';
- /**
- * Set additional classnames to the `dd` element.
- */
- valueClassName?: string;
- /**
- * If true, use a slash to delimitate multiple values.
- */
- withSeparator?: DescriptionListGroupProps['withSeparator'];
-};
-
-/**
- * DescriptionList component
- *
- * Render a description list.
- */
-export const DescriptionList: FC<DescriptionListProps> = ({
- className = '',
- groupClassName = '',
- items,
- labelClassName = '',
- layout = 'column',
- valueClassName = '',
- withSeparator,
- ...props
-}) => {
- const layoutModifier = `list--${layout}`;
- const listClass = `${styles.list} ${styles[layoutModifier]} ${className}`;
-
- /**
- * Retrieve the description list items.
- *
- * @param {DescriptionListGroup[]} listItems - An array of items.
- * @returns {JSX.Element[]} The description list items.
- */
- const getItems = (listItems: DescriptionListItem[]): JSX.Element[] => {
- return listItems.map(({ id, layout: itemLayout, label, value }) => {
- return (
- <DescriptionListGroup
- key={id}
- label={label}
- value={value}
- layout={itemLayout}
- className={groupClassName}
- descriptionClassName={valueClassName}
- termClassName={labelClassName}
- withSeparator={withSeparator}
- />
- );
- });
- };
-
- return (
- <dl {...props} className={listClass}>
- {getItems(items)}
- </dl>
- );
-};
diff --git a/src/components/atoms/lists/description-list/description-list.module.scss b/src/components/atoms/lists/description-list/description-list.module.scss
new file mode 100644
index 0000000..951e1ee
--- /dev/null
+++ b/src/components/atoms/lists/description-list/description-list.module.scss
@@ -0,0 +1,28 @@
+@use "../../../../styles/abstracts/placeholders";
+
+.term {
+ @extend %term;
+}
+
+.description {
+ @extend %description;
+}
+
+.group {
+ width: fit-content;
+}
+
+.list {
+ margin: 0;
+}
+
+.group,
+.list {
+ &--inline {
+ @extend %inline-description-list;
+ }
+
+ &--stack {
+ @extend %stack-description-list;
+ }
+}
diff --git a/src/components/atoms/lists/description-list/description-list.stories.tsx b/src/components/atoms/lists/description-list/description-list.stories.tsx
new file mode 100644
index 0000000..d051fcd
--- /dev/null
+++ b/src/components/atoms/lists/description-list/description-list.stories.tsx
@@ -0,0 +1,150 @@
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Description } from './description';
+import { DescriptionList } from './description-list';
+import { Group } from './group';
+import { Term } from './term';
+
+/**
+ * DescriptionList - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Lists/DescriptionList',
+ component: DescriptionList,
+ args: {
+ isInline: false,
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the list wrapper',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof DescriptionList>;
+
+const Template: ComponentStory<typeof DescriptionList> = (args) => (
+ <DescriptionList {...args} />
+);
+
+/**
+ * Description List Stories - Single term, single description
+ */
+export const SingleTermSingleDescription = Template.bind({});
+SingleTermSingleDescription.args = {
+ children: (
+ <>
+ <Term>A term</Term>
+ <Description>A description of the term.</Description>
+ </>
+ ),
+};
+
+/**
+ * Description List Stories - Multiple terms, single description
+ */
+export const MultipleTermsSingleDescription = Template.bind({});
+MultipleTermsSingleDescription.args = {
+ children: (
+ <>
+ <Term>A first term</Term>
+ <Term>A second term</Term>
+ <Term>A third term</Term>
+ <Description>A description of the term.</Description>
+ </>
+ ),
+};
+
+/**
+ * Description List Stories - Single term, multiple descriptions
+ */
+export const SingleTermMultipleDescriptions = Template.bind({});
+SingleTermMultipleDescriptions.args = {
+ children: (
+ <>
+ <Term>A term</Term>
+ <Description>A first description of the term.</Description>
+ <Description>A second description of the term.</Description>
+ <Description>A third description of the term.</Description>
+ </>
+ ),
+};
+
+/**
+ * Description List Stories - Multiple terms, multiple descriptions
+ */
+export const MultipleTermsMultipleDescriptions = Template.bind({});
+MultipleTermsMultipleDescriptions.args = {
+ children: (
+ <>
+ <Term>A first term</Term>
+ <Term>A second term</Term>
+ <Term>A third term</Term>
+ <Description>A first description of the term.</Description>
+ <Description>A second description of the term.</Description>
+ <Description>A third description of the term.</Description>
+ </>
+ ),
+};
+
+/**
+ * Description List Stories - Group of terms & descriptions
+ */
+export const GroupOfTermsDescriptions = Template.bind({});
+GroupOfTermsDescriptions.args = {
+ children: (
+ <>
+ <Group>
+ <Term>A term</Term>
+ <Description>A description of the term.</Description>
+ </Group>
+ <Group>
+ <Term>Another term</Term>
+ <Description>A description of the other term.</Description>
+ </Group>
+ </>
+ ),
+};
+
+/**
+ * Description List Stories - Inlined list of term and descriptions
+ */
+export const InlinedList = Template.bind({});
+InlinedList.args = {
+ children: (
+ <>
+ <Term>A term:</Term>
+ <Description>A first description of the term.</Description>
+ <Description>A second description of the term.</Description>
+ <Description>A third description of the term.</Description>
+ </>
+ ),
+ isInline: true,
+ spacing: 'xs',
+};
+
+/**
+ * Description List Stories - Inlined group of terms & descriptions
+ */
+export const InlinedGroupOfTermsDescriptions = Template.bind({});
+InlinedGroupOfTermsDescriptions.args = {
+ children: (
+ <>
+ <Group isInline spacing="2xs">
+ <Term>A term:</Term>
+ <Description>A description of the term.</Description>
+ </Group>
+ <Group isInline spacing="2xs">
+ <Term>Another term:</Term>
+ <Description>A description of the other term.</Description>
+ </Group>
+ </>
+ ),
+};
diff --git a/src/components/atoms/lists/description-list/description-list.test.tsx b/src/components/atoms/lists/description-list/description-list.test.tsx
new file mode 100644
index 0000000..3f9a1b5
--- /dev/null
+++ b/src/components/atoms/lists/description-list/description-list.test.tsx
@@ -0,0 +1,70 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { Description } from './description';
+import { DescriptionList } from './description-list';
+import { Group } from './group';
+import { Term } from './term';
+
+describe('DescriptionList', () => {
+ it('renders a list of terms and description', () => {
+ const term = 'A term';
+ const desc = 'A description of the term.';
+
+ render(
+ <DescriptionList>
+ <Term>{term}</Term>
+ <Description>{desc}</Description>
+ </DescriptionList>
+ );
+
+ expect(rtlScreen.getByRole('definition')).toHaveTextContent(desc);
+ expect(rtlScreen.getByRole('term')).toHaveTextContent(term);
+ });
+
+ it('can renders a list of terms and description wrapped in a div', () => {
+ const term = 'A term';
+ const desc = 'A description of the term.';
+
+ render(
+ <DescriptionList>
+ <Group>
+ <Term>{term}</Term>
+ <Description>{desc}</Description>
+ </Group>
+ </DescriptionList>
+ );
+
+ expect(rtlScreen.getByRole('definition')).toHaveTextContent(desc);
+ expect(rtlScreen.getByRole('term')).toHaveTextContent(term);
+ });
+
+ it('can render terms and description inlined', () => {
+ const term = 'A term';
+ const desc = 'A description of the term.';
+
+ render(
+ <DescriptionList isInline>
+ <Term>{term}</Term>
+ <Description>{desc}</Description>
+ </DescriptionList>
+ );
+
+ const list = rtlScreen.getByRole('term').parentElement;
+ expect(list).toHaveClass('list--inline');
+ });
+
+ it('can render terms and description stacked', () => {
+ const term = 'A term';
+ const desc = 'A description of the term.';
+
+ render(
+ <DescriptionList>
+ <Term>{term}</Term>
+ <Description>{desc}</Description>
+ </DescriptionList>
+ );
+
+ const list = rtlScreen.getByRole('term').parentElement;
+ expect(list).toHaveClass('list--stack');
+ });
+});
diff --git a/src/components/atoms/lists/description-list/description-list.tsx b/src/components/atoms/lists/description-list/description-list.tsx
new file mode 100644
index 0000000..cc225fe
--- /dev/null
+++ b/src/components/atoms/lists/description-list/description-list.tsx
@@ -0,0 +1,67 @@
+import {
+ forwardRef,
+ type CSSProperties,
+ type HTMLAttributes,
+ type ReactNode,
+ type ForwardRefRenderFunction,
+} from 'react';
+import type { Spacing } from '../../../../types';
+import styles from './description-list.module.scss';
+
+export type DescriptionListProps = Omit<
+ HTMLAttributes<HTMLDListElement>,
+ 'children'
+> & {
+ /**
+ * The list items or groups.
+ */
+ children: ReactNode;
+ /**
+ * Should the list be inlined?
+ *
+ * @default false
+ */
+ isInline?: boolean;
+ /**
+ * Define the spacing between list items.
+ *
+ * @default null
+ */
+ spacing?: Spacing | null;
+};
+
+const DescriptionListWithRef: ForwardRefRenderFunction<
+ HTMLDListElement,
+ DescriptionListProps
+> = (
+ {
+ children,
+ className = '',
+ isInline = false,
+ spacing = null,
+ style,
+ ...props
+ },
+ ref
+) => {
+ const itemSpacing = spacing === null ? 0 : `var(--spacing-${spacing})`;
+ const layoutClass = styles[isInline ? 'list--inline' : 'list--stack'];
+ const listClass = `${styles.list} ${layoutClass} ${className}`;
+ const listStyles = {
+ ...style,
+ '--itemSpacing': itemSpacing,
+ } as CSSProperties;
+
+ return (
+ <dl {...props} className={listClass} ref={ref} style={listStyles}>
+ {children}
+ </dl>
+ );
+};
+
+/**
+ * DescriptionList component
+ *
+ * Render a description list.
+ */
+export const DescriptionList = forwardRef(DescriptionListWithRef);
diff --git a/src/components/atoms/lists/description-list/description.tsx b/src/components/atoms/lists/description-list/description.tsx
new file mode 100644
index 0000000..9fa7ecd
--- /dev/null
+++ b/src/components/atoms/lists/description-list/description.tsx
@@ -0,0 +1,28 @@
+import {
+ forwardRef,
+ type ForwardRefRenderFunction,
+ type HTMLAttributes,
+} from 'react';
+import styles from './description-list.module.scss';
+
+export type DescriptionProps = HTMLAttributes<HTMLElement>;
+
+const DescriptionWithRef: ForwardRefRenderFunction<
+ HTMLElement,
+ DescriptionProps
+> = ({ children, className = '', ...props }, ref) => {
+ const descriptionClass = `${styles.description} ${className}`;
+
+ return (
+ <dd {...props} className={descriptionClass} ref={ref}>
+ {children}
+ </dd>
+ );
+};
+
+/**
+ * Description component.
+ *
+ * Use it inside a `DescriptionList` or a `Group` component.
+ */
+export const Description = forwardRef(DescriptionWithRef);
diff --git a/src/components/atoms/lists/description-list/group.tsx b/src/components/atoms/lists/description-list/group.tsx
new file mode 100644
index 0000000..2d1fb4b
--- /dev/null
+++ b/src/components/atoms/lists/description-list/group.tsx
@@ -0,0 +1,62 @@
+import {
+ forwardRef,
+ type CSSProperties,
+ type HTMLAttributes,
+ type ReactNode,
+ type ForwardRefRenderFunction,
+} from 'react';
+import type { Spacing } from '../../../../types';
+import styles from './description-list.module.scss';
+
+export type GroupProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
+ /**
+ * The term(s) and description(s) of a description list.
+ */
+ children: ReactNode;
+ /**
+ * Should the term & description in the group be inlined?
+ *
+ * @default false
+ */
+ isInline?: boolean;
+ /**
+ * Define the spacing between list items.
+ *
+ * @default null
+ */
+ spacing?: Spacing | null;
+};
+
+const GroupWithRef: ForwardRefRenderFunction<HTMLDivElement, GroupProps> = (
+ {
+ children,
+ className = '',
+ isInline = false,
+ spacing = null,
+ style,
+ ...props
+ },
+ ref
+) => {
+ const itemSpacing = spacing === null ? 0 : `var(--spacing-${spacing})`;
+ const layoutClass = styles[isInline ? 'group--inline' : 'group--stack'];
+ const groupClass = `${styles.group} ${layoutClass} ${className}`;
+ const groupStyles = {
+ ...style,
+ '--itemSpacing': itemSpacing,
+ } as CSSProperties;
+
+ return (
+ <div {...props} className={groupClass} ref={ref} style={groupStyles}>
+ {children}
+ </div>
+ );
+};
+
+/**
+ * Group component.
+ *
+ * Use it to wrap `Description` and `Term` components in a `DescriptionList`
+ * component.
+ */
+export const Group = forwardRef(GroupWithRef);
diff --git a/src/components/atoms/lists/description-list/index.ts b/src/components/atoms/lists/description-list/index.ts
new file mode 100644
index 0000000..7f67579
--- /dev/null
+++ b/src/components/atoms/lists/description-list/index.ts
@@ -0,0 +1,4 @@
+export * from './description';
+export * from './description-list';
+export * from './group';
+export * from './term';
diff --git a/src/components/atoms/lists/description-list/term.tsx b/src/components/atoms/lists/description-list/term.tsx
new file mode 100644
index 0000000..0d21f96
--- /dev/null
+++ b/src/components/atoms/lists/description-list/term.tsx
@@ -0,0 +1,28 @@
+import {
+ forwardRef,
+ type ForwardRefRenderFunction,
+ type HTMLAttributes,
+} from 'react';
+import styles from './description-list.module.scss';
+
+export type TermProps = HTMLAttributes<HTMLElement>;
+
+const TermWithRef: ForwardRefRenderFunction<HTMLElement, TermProps> = (
+ { children, className = '', ...props },
+ ref
+) => {
+ const termClass = `${styles.term} ${className}`;
+
+ return (
+ <dt {...props} className={termClass} ref={ref}>
+ {children}
+ </dt>
+ );
+};
+
+/**
+ * Term component.
+ *
+ * Use it inside a `DescriptionList` or a `Group` component.
+ */
+export const Term = forwardRef(TermWithRef);
diff --git a/src/components/atoms/lists/index.ts b/src/components/atoms/lists/index.ts
index d16fb34..59079a5 100644
--- a/src/components/atoms/lists/index.ts
+++ b/src/components/atoms/lists/index.ts
@@ -1,3 +1,2 @@
export * from './description-list';
-export * from './description-list-group';
export * from './list';