summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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.tsx73
4 files changed, 218 insertions, 0 deletions
diff --git a/src/components/atoms/lists/list.module.scss b/src/components/atoms/lists/list.module.scss
new file mode 100644
index 0000000..c6fbf53
--- /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);
+ }
+
+ &__item {
+ &:not(:last-child) {
+ margin-bottom: 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);
+ }
+}
diff --git a/src/components/atoms/lists/list.stories.tsx b/src/components/atoms/lists/list.stories.tsx
new file mode 100644
index 0000000..6256e81
--- /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: {
+ classes: {
+ control: {
+ type: 'text',
+ },
+ description: 'Add additional classes to the list element.',
+ table: {
+ category: 'Options',
+ },
+ 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..8ce8437
--- /dev/null
+++ b/src/components/atoms/lists/list.tsx
@@ -0,0 +1,73 @@
+import { FC } 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 = {
+ /**
+ * Add additional classes to the list element.
+ */
+ classes?: string;
+ /**
+ * An array of list items.
+ */
+ items: ListItem[];
+ /**
+ * The list kind (ordered or unordered).
+ */
+ kind?: 'ordered' | 'unordered';
+};
+
+/**
+ * List component
+ *
+ * Render either an ordered or an unordered list.
+ */
+const List: FC<ListProps> = ({ classes, items, kind = 'unordered' }) => {
+ const ListTag = kind === 'ordered' ? 'ol' : 'ul';
+ const additionalClasses = classes || '';
+ const kindClass = `list--${kind}`;
+
+ /**
+ * 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}>
+ {value}
+ {child && (
+ <ListTag
+ className={`${styles.list} ${styles[kindClass]} ${additionalClasses}`}
+ >
+ {getItems(child)}
+ </ListTag>
+ )}
+ </li>
+ ));
+ };
+
+ return (
+ <ListTag
+ className={`${styles.list} ${styles[kindClass]} ${additionalClasses}`}
+ >
+ {getItems(items)}
+ </ListTag>
+ );
+};
+
+export default List;