aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/lists
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/atoms/lists')
-rw-r--r--src/components/atoms/lists/description-list.module.scss54
-rw-r--r--src/components/atoms/lists/description-list.stories.tsx73
-rw-r--r--src/components/atoms/lists/description-list.test.tsx20
-rw-r--r--src/components/atoms/lists/description-list.tsx100
-rw-r--r--src/components/atoms/lists/list.module.scss39
-rw-r--r--src/components/atoms/lists/list.stories.tsx80
-rw-r--r--src/components/atoms/lists/list.test.tsx26
-rw-r--r--src/components/atoms/lists/list.tsx87
8 files changed, 479 insertions, 0 deletions
diff --git a/src/components/atoms/lists/description-list.module.scss b/src/components/atoms/lists/description-list.module.scss
new file mode 100644
index 0000000..caa2711
--- /dev/null
+++ b/src/components/atoms/lists/description-list.module.scss
@@ -0,0 +1,54 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.list {
+ display: flex;
+ flex-flow: column wrap;
+ gap: var(--spacing-2xs);
+ margin: 0;
+
+ &__term {
+ flex: 0 0 max-content;
+ color: var(--color-fg-light);
+ font-weight: 600;
+ }
+
+ &__description {
+ flex: 0 0 auto;
+ margin: 0;
+ }
+
+ &__item {
+ display: flex;
+ }
+
+ &--inline &__item {
+ flex-flow: column wrap;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("xs") {
+ flex-flow: row wrap;
+ gap: var(--spacing-2xs);
+
+ .list__description:not(:first-of-type) {
+ &::before {
+ content: "/";
+ margin-right: var(--spacing-2xs);
+ }
+ }
+ }
+ }
+ }
+
+ &--column#{&}--responsive {
+ @include mix.media("screen") {
+ @include mix.dimensions("xs") {
+ flex-flow: row wrap;
+ gap: var(--spacing-lg);
+ }
+ }
+ }
+
+ &--column &__item {
+ flex-flow: column wrap;
+ }
+}
diff --git a/src/components/atoms/lists/description-list.stories.tsx b/src/components/atoms/lists/description-list.stories.tsx
new file mode 100644
index 0000000..66d94af
--- /dev/null
+++ b/src/components/atoms/lists/description-list.stories.tsx
@@ -0,0 +1,73 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import DescriptionListComponent, {
+ DescriptionListItem,
+} from './description-list';
+
+export default {
+ title: 'Atoms/Lists',
+ component: DescriptionListComponent,
+ args: {
+ layout: 'column',
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the list wrapper',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ items: {
+ control: {
+ type: null,
+ },
+ description: 'The list items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ layout: {
+ control: {
+ type: 'select',
+ },
+ description: 'The list layout.',
+ options: ['column', 'inline'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'column' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof DescriptionListComponent>;
+
+const Template: ComponentStory<typeof DescriptionListComponent> = (args) => (
+ <DescriptionListComponent {...args} />
+);
+
+const items: DescriptionListItem[] = [
+ { id: 'term-1', term: 'Term 1:', value: ['Value for term 1'] },
+ { id: 'term-2', term: 'Term 2:', value: ['Value for term 2'] },
+ {
+ id: 'term-3',
+ term: 'Term 3:',
+ value: ['Value 1 for term 3', 'Value 2 for term 3', 'Value 3 for term 3'],
+ },
+ { id: 'term-4', term: 'Term 4:', value: ['Value for term 4'] },
+];
+
+export const DescriptionList = Template.bind({});
+DescriptionList.args = {
+ items,
+};
diff --git a/src/components/atoms/lists/description-list.test.tsx b/src/components/atoms/lists/description-list.test.tsx
new file mode 100644
index 0000000..d3f7045
--- /dev/null
+++ b/src/components/atoms/lists/description-list.test.tsx
@@ -0,0 +1,20 @@
+import { render } from '@test-utils';
+import DescriptionList, { DescriptionListItem } from './description-list';
+
+const items: DescriptionListItem[] = [
+ { id: 'term-1', term: 'Term 1:', value: ['Value for term 1'] },
+ { id: 'term-2', term: 'Term 2:', value: ['Value for term 2'] },
+ {
+ id: 'term-3',
+ term: 'Term 3:',
+ value: ['Value 1 for term 3', 'Value 2 for term 3', 'Value 3 for term 3'],
+ },
+ { id: 'term-4', term: '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
new file mode 100644
index 0000000..0a92465
--- /dev/null
+++ b/src/components/atoms/lists/description-list.tsx
@@ -0,0 +1,100 @@
+import { VFC } from 'react';
+import styles from './description-list.module.scss';
+
+export type DescriptionListItem = {
+ /**
+ * The item id.
+ */
+ id: string;
+ /**
+ * A list term.
+ */
+ term: string;
+ /**
+ * An array of values for the list term.
+ */
+ value: any[];
+};
+
+export type DescriptionListProps = {
+ /**
+ * Set additional classnames to the list wrapper.
+ */
+ className?: string;
+ /**
+ * Set additional classnames to the `dd` element.
+ */
+ descriptionClassName?: string;
+ /**
+ * Set additional classnames to the `dt`/`dd` couple wrapper.
+ */
+ groupClassName?: string;
+ /**
+ * The list items.
+ */
+ items: DescriptionListItem[];
+ /**
+ * The list items layout. Default: column.
+ */
+ layout?: 'inline' | 'column';
+ /**
+ * Define if the layout should automatically create rows/columns.
+ */
+ responsiveLayout?: boolean;
+ /**
+ * Set additional classnames to the `dt` element.
+ */
+ termClassName?: string;
+};
+
+/**
+ * DescriptionList component
+ *
+ * Render a description list.
+ */
+const DescriptionList: VFC<DescriptionListProps> = ({
+ className = '',
+ descriptionClassName = '',
+ groupClassName = '',
+ items,
+ layout = 'column',
+ responsiveLayout = false,
+ termClassName = '',
+}) => {
+ const layoutModifier = `list--${layout}`;
+ const responsiveModifier = responsiveLayout ? 'list--responsive' : '';
+
+ /**
+ * Retrieve the description list items wrapped in a div element.
+ *
+ * @param {DescriptionListItem[]} listItems - An array of term and description couples.
+ * @returns {JSX.Element[]} The description list items.
+ */
+ const getItems = (listItems: DescriptionListItem[]): JSX.Element[] => {
+ return listItems.map(({ id, term, value }) => {
+ return (
+ <div key={id} className={`${styles.list__item} ${groupClassName}`}>
+ <dt className={`${styles.list__term} ${termClassName}`}>{term}</dt>
+ {value.map((currentValue, index) => (
+ <dd
+ key={`${id}-${index}`}
+ className={`${styles.list__description} ${descriptionClassName}`}
+ >
+ {currentValue}
+ </dd>
+ ))}
+ </div>
+ );
+ });
+ };
+
+ return (
+ <dl
+ className={`${styles.list} ${styles[layoutModifier]} ${styles[responsiveModifier]} ${className}`}
+ >
+ {getItems(items)}
+ </dl>
+ );
+};
+
+export default DescriptionList;
diff --git a/src/components/atoms/lists/list.module.scss b/src/components/atoms/lists/list.module.scss
new file mode 100644
index 0000000..df3b49c
--- /dev/null
+++ b/src/components/atoms/lists/list.module.scss
@@ -0,0 +1,39 @@
+.list {
+ margin: 0;
+
+ ::marker {
+ color: var(--color-primary-dark);
+ }
+
+ & & {
+ margin-top: var(--spacing-2xs);
+ }
+
+ &--ordered {
+ padding: 0;
+ counter-reset: li;
+ list-style-type: none;
+ }
+
+ &--ordered &__item {
+ display: table;
+ counter-increment: li;
+
+ &::before {
+ content: counters(li, ".") ". ";
+ display: table-cell;
+ padding-right: var(--spacing-2xs);
+ color: var(--color-secondary);
+ }
+ }
+
+ &--unordered {
+ padding: 0 0 0 var(--spacing-sm);
+ }
+
+ &--has-margin &__item {
+ &:not(:last-child) {
+ margin-bottom: var(--spacing-2xs);
+ }
+ }
+}
diff --git a/src/components/atoms/lists/list.stories.tsx b/src/components/atoms/lists/list.stories.tsx
new file mode 100644
index 0000000..30079cb
--- /dev/null
+++ b/src/components/atoms/lists/list.stories.tsx
@@ -0,0 +1,80 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ListComponent, { type ListItem } from './list';
+
+export default {
+ title: 'Atoms/Lists',
+ component: ListComponent,
+ args: {
+ kind: 'unordered',
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the list wrapper',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ items: {
+ control: {
+ type: null,
+ },
+ description: 'The list items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ kind: {
+ control: {
+ type: 'select',
+ },
+ description: 'The list kind: ordered or unordered.',
+ options: ['ordered', 'unordered'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'unordered' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof ListComponent>;
+
+const Template: ComponentStory<typeof ListComponent> = (args) => (
+ <ListComponent {...args} />
+);
+
+const items: ListItem[] = [
+ { id: 'item-1', value: 'Item 1' },
+ { id: 'item-2', value: 'Item 2' },
+ {
+ child: [
+ { id: 'nested-item-1', value: 'Nested item 1' },
+ { id: 'nested-item-2', value: 'Nested item 2' },
+ ],
+ id: 'item-3',
+ value: 'Item 3',
+ },
+ { id: 'item-4', value: 'Item 4' },
+];
+
+export const Unordered = Template.bind({});
+Unordered.args = {
+ items,
+};
+
+export const Ordered = Template.bind({});
+Ordered.args = {
+ items,
+ kind: 'ordered',
+};
diff --git a/src/components/atoms/lists/list.test.tsx b/src/components/atoms/lists/list.test.tsx
new file mode 100644
index 0000000..fcf8813
--- /dev/null
+++ b/src/components/atoms/lists/list.test.tsx
@@ -0,0 +1,26 @@
+import { render, screen } from '@test-utils';
+import List, { type ListItem } from './list';
+
+const items: ListItem[] = [
+ { id: 'item-1', value: 'Item 1' },
+ { id: 'item-2', value: 'Item 2' },
+ {
+ child: [
+ { id: 'nested-item-1', value: 'Nested item 1' },
+ { id: 'nested-item-2', value: 'Nested item 2' },
+ ],
+ id: 'item-3',
+ value: 'Item 3',
+ },
+ { id: 'item-4', value: 'Item 4' },
+];
+
+describe('List', () => {
+ it('renders a nested unordered list', () => {
+ render(<List items={items} />);
+ const listItems = screen.getAllByRole('list');
+ listItems.forEach((listItem) =>
+ expect(listItem).toHaveClass('list--unordered')
+ );
+ });
+});
diff --git a/src/components/atoms/lists/list.tsx b/src/components/atoms/lists/list.tsx
new file mode 100644
index 0000000..d100a31
--- /dev/null
+++ b/src/components/atoms/lists/list.tsx
@@ -0,0 +1,87 @@
+import { VFC } from 'react';
+import styles from './list.module.scss';
+
+export type ListItem = {
+ /**
+ * Nested list.
+ */
+ child?: ListItem[];
+ /**
+ * Item id.
+ */
+ id: string;
+ /**
+ * Item value.
+ */
+ value: any;
+};
+
+export type ListProps = {
+ /**
+ * Set additional classnames to the list wrapper.
+ */
+ className?: string;
+ /**
+ * An array of list items.
+ */
+ items: ListItem[];
+ /**
+ * Set additional classnames to the list items.
+ */
+ itemsClassName?: string;
+ /**
+ * The list kind (ordered or unordered).
+ */
+ kind?: 'ordered' | 'unordered';
+ /**
+ * Set margin between list items. Default: true.
+ */
+ withMargin?: boolean;
+};
+
+/**
+ * List component
+ *
+ * Render either an ordered or an unordered list.
+ */
+const List: VFC<ListProps> = ({
+ className = '',
+ items,
+ itemsClassName = '',
+ kind = 'unordered',
+ withMargin = true,
+}) => {
+ const ListTag = kind === 'ordered' ? 'ol' : 'ul';
+ const kindClass = `list--${kind}`;
+ const marginClass = withMargin ? 'list--has-margin' : 'list--no-margin';
+
+ /**
+ * Retrieve the list items.
+ * @param array - An array of items.
+ * @returns {JSX.Element[]} - An array of li elements.
+ */
+ const getItems = (array: ListItem[]): JSX.Element[] => {
+ return array.map(({ child, id, value }) => (
+ <li key={id} className={`${styles.list__item} ${itemsClassName}`}>
+ {value}
+ {child && (
+ <ListTag
+ className={`${styles.list} ${styles[kindClass]} ${styles[marginClass]} ${className}`}
+ >
+ {getItems(child)}
+ </ListTag>
+ )}
+ </li>
+ ));
+ };
+
+ return (
+ <ListTag
+ className={`${styles.list} ${styles[kindClass]} ${styles[marginClass]} ${className}`}
+ >
+ {getItems(items)}
+ </ListTag>
+ );
+};
+
+export default List;