summaryrefslogtreecommitdiffstats
path: root/src/components/atoms/lists
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-05-24 19:35:12 +0200
committerGitHub <noreply@github.com>2022-05-24 19:35:12 +0200
commitc85ab5ad43ccf52881ee224672c41ec30021cf48 (patch)
tree8058808d9bfca19383f120c46b34d99ff2f89f63 /src/components/atoms/lists
parent52404177c07a2aab7fc894362fb3060dff2431a0 (diff)
parent11b9de44a4b2f305a6a484187805e429b2767118 (diff)
refactor: use storybook and atomic design (#16)
BREAKING CHANGE: rewrite most of the Typescript types, so the content format (the meta in particular) needs to be updated.
Diffstat (limited to 'src/components/atoms/lists')
-rw-r--r--src/components/atoms/lists/description-list-item.module.scss40
-rw-r--r--src/components/atoms/lists/description-list-item.stories.tsx132
-rw-r--r--src/components/atoms/lists/description-list-item.test.tsx17
-rw-r--r--src/components/atoms/lists/description-list-item.tsx73
-rw-r--r--src/components/atoms/lists/description-list.module.scss17
-rw-r--r--src/components/atoms/lists/description-list.stories.tsx131
-rw-r--r--src/components/atoms/lists/description-list.test.tsx20
-rw-r--r--src/components/atoms/lists/description-list.tsx103
-rw-r--r--src/components/atoms/lists/list.module.scss45
-rw-r--r--src/components/atoms/lists/list.stories.tsx111
-rw-r--r--src/components/atoms/lists/list.test.tsx26
-rw-r--r--src/components/atoms/lists/list.tsx79
12 files changed, 794 insertions, 0 deletions
diff --git a/src/components/atoms/lists/description-list-item.module.scss b/src/components/atoms/lists/description-list-item.module.scss
new file mode 100644
index 0000000..aba90ce
--- /dev/null
+++ b/src/components/atoms/lists/description-list-item.module.scss
@@ -0,0 +1,40 @@
+.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-item.stories.tsx b/src/components/atoms/lists/description-list-item.stories.tsx
new file mode 100644
index 0000000..c7beb0d
--- /dev/null
+++ b/src/components/atoms/lists/description-list-item.stories.tsx
@@ -0,0 +1,132 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import DescriptionListItemComponent from './description-list-item';
+
+export default {
+ title: 'Atoms/Typography/Lists/DescriptionList/Item',
+ component: DescriptionListItemComponent,
+ 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 DescriptionListItemComponent>;
+
+const Template: ComponentStory<typeof DescriptionListItemComponent> = (
+ args
+) => <DescriptionListItemComponent {...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-item.test.tsx b/src/components/atoms/lists/description-list-item.test.tsx
new file mode 100644
index 0000000..730a52f
--- /dev/null
+++ b/src/components/atoms/lists/description-list-item.test.tsx
@@ -0,0 +1,17 @@
+import { render, screen } from '@test-utils';
+import DescriptionListItem from './description-list-item';
+
+const itemLabel = 'Repellendus corporis facilis';
+const itemValue = ['quos', 'eum'];
+
+describe('DescriptionListItem', () => {
+ it('renders a couple of label', () => {
+ render(<DescriptionListItem label={itemLabel} value={itemValue} />);
+ expect(screen.getByRole('term')).toHaveTextContent(itemLabel);
+ });
+
+ it('renders the right number of values', () => {
+ render(<DescriptionListItem label={itemLabel} value={itemValue} />);
+ expect(screen.getAllByRole('definition')).toHaveLength(itemValue.length);
+ });
+});
diff --git a/src/components/atoms/lists/description-list-item.tsx b/src/components/atoms/lists/description-list-item.tsx
new file mode 100644
index 0000000..9505d01
--- /dev/null
+++ b/src/components/atoms/lists/description-list-item.tsx
@@ -0,0 +1,73 @@
+import { FC, ReactNode, useId } from 'react';
+import styles from './description-list-item.module.scss';
+
+export type ItemLayout = 'inline' | 'inline-values' | 'stacked';
+
+export type DescriptionListItemProps = {
+ /**
+ * 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.
+ */
+const DescriptionListItem: FC<DescriptionListItemProps> = ({
+ 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];
+
+ return (
+ <div
+ className={`${styles.wrapper} ${layoutStyles} ${separatorStyles} ${className}`}
+ >
+ <dt className={`${styles.term} ${termClassName}`}>{label}</dt>
+ {itemValues.map((currentValue, index) => (
+ <dd
+ key={`${id}-${index}`}
+ className={`${styles.description} ${descriptionClassName}`}
+ >
+ {currentValue}
+ </dd>
+ ))}
+ </div>
+ );
+};
+
+export default DescriptionListItem;
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..9e913d4
--- /dev/null
+++ b/src/components/atoms/lists/description-list.module.scss
@@ -0,0 +1,17 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.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
new file mode 100644
index 0000000..347fd78
--- /dev/null
+++ b/src/components/atoms/lists/description-list.stories.tsx
@@ -0,0 +1,131 @@
+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
new file mode 100644
index 0000000..83e405f
--- /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', 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
new file mode 100644
index 0000000..a8e2d53
--- /dev/null
+++ b/src/components/atoms/lists/description-list.tsx
@@ -0,0 +1,103 @@
+import { FC } from 'react';
+import DescriptionListItem, {
+ type DescriptionListItemProps,
+} from './description-list-item';
+import styles from './description-list.module.scss';
+
+export type DescriptionListItem = {
+ /**
+ * The item id.
+ */
+ id: string;
+ /**
+ * The list item layout.
+ */
+ layout?: DescriptionListItemProps['layout'];
+ /**
+ * A list label.
+ */
+ label: DescriptionListItemProps['label'];
+ /**
+ * An array of values for the list item.
+ */
+ value: DescriptionListItemProps['value'];
+};
+
+export type DescriptionListProps = {
+ /**
+ * Set additional classnames to the list wrapper.
+ */
+ className?: string;
+ /**
+ * 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?: DescriptionListItemProps['withSeparator'];
+};
+
+/**
+ * DescriptionList component
+ *
+ * Render a description list.
+ */
+const DescriptionList: FC<DescriptionListProps> = ({
+ className = '',
+ groupClassName = '',
+ items,
+ labelClassName = '',
+ layout = 'column',
+ valueClassName = '',
+ withSeparator,
+}) => {
+ const layoutModifier = `list--${layout}`;
+
+ /**
+ * Retrieve the description list items.
+ *
+ * @param {DescriptionListItem[]} 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 (
+ <DescriptionListItem
+ key={id}
+ label={label}
+ value={value}
+ layout={itemLayout}
+ className={groupClassName}
+ descriptionClassName={valueClassName}
+ termClassName={labelClassName}
+ withSeparator={withSeparator}
+ />
+ );
+ });
+ };
+
+ return (
+ <dl className={`${styles.list} ${styles[layoutModifier]} ${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..95f9b40
--- /dev/null
+++ b/src/components/atoms/lists/list.module.scss
@@ -0,0 +1,45 @@
+@use "@styles/abstracts/placeholders";
+
+.list {
+ margin: 0;
+
+ ::marker {
+ color: var(--color-primary-dark);
+ }
+
+ &--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);
+ }
+
+ &--flex {
+ @extend %reset-list;
+
+ display: flex;
+ flex-flow: row wrap;
+ gap: var(--spacing-sm);
+ }
+
+ &--flex &--flex {
+ display: initial;
+ position: relative;
+ top: 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..eac3cd3
--- /dev/null
+++ b/src/components/atoms/lists/list.stories.tsx
@@ -0,0 +1,111 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ListComponent, { type ListItem } from './list';
+
+/**
+ * List - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Typography/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: {},
+ },
+ },
+ itemsClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the list items.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ kind: {
+ control: {
+ type: 'select',
+ },
+ description: 'The list kind: flex, ordered or unordered.',
+ options: ['flex', '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' },
+];
+
+/**
+ * List Stories - Flex list
+ */
+export const Flex = Template.bind({});
+Flex.args = {
+ items,
+ kind: 'flex',
+};
+
+/**
+ * List Stories - Ordered list
+ */
+export const Ordered = Template.bind({});
+Ordered.args = {
+ items,
+ kind: 'ordered',
+};
+
+/**
+ * List Stories - Unordered list
+ */
+export const Unordered = Template.bind({});
+Unordered.args = {
+ items,
+};
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..aa0a241
--- /dev/null
+++ b/src/components/atoms/lists/list.tsx
@@ -0,0 +1,79 @@
+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 = {
+ /**
+ * 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.
+ */
+ kind?: 'ordered' | 'unordered' | 'flex';
+};
+
+/**
+ * List component
+ *
+ * Render either an ordered or an unordered list.
+ */
+const List: FC<ListProps> = ({
+ className = '',
+ items,
+ itemsClassName = '',
+ kind = 'unordered',
+}) => {
+ const ListTag = kind === 'ordered' ? 'ol' : 'ul';
+ 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} ${itemsClassName}`}>
+ {value}
+ {child && (
+ <ListTag
+ className={`${styles.list} ${styles[kindClass]} ${className}`}
+ >
+ {getItems(child)}
+ </ListTag>
+ )}
+ </li>
+ ));
+ };
+
+ return (
+ <ListTag className={`${styles.list} ${styles[kindClass]} ${className}`}>
+ {getItems(items)}
+ </ListTag>
+ );
+};
+
+export default List;