aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/lists/description-list
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/atoms/lists/description-list')
-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
8 files changed, 437 insertions, 0 deletions
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);