diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-04-05 23:10:44 +0200 | 
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-04-05 23:10:44 +0200 | 
| commit | 655ed38c6cd53a19c6ba1ebab5f2429441b99a58 (patch) | |
| tree | b7107d8c89580b7b13a35c6d4453bb6362e56a63 /src/components/atoms | |
| parent | 57568e61c22c41c073f4db59992735387e8372fe (diff) | |
chore: add a List component
Diffstat (limited to 'src/components/atoms')
| -rw-r--r-- | src/components/atoms/lists/list.module.scss | 39 | ||||
| -rw-r--r-- | src/components/atoms/lists/list.stories.tsx | 80 | ||||
| -rw-r--r-- | src/components/atoms/lists/list.test.tsx | 26 | ||||
| -rw-r--r-- | src/components/atoms/lists/list.tsx | 73 | 
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; | 
