diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-09-29 21:29:45 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:14:41 +0100 |
| commit | 4f768afe543bbf9e1857c41d03804f8e37ab3512 (patch) | |
| tree | d751219a147688b5665c51db3c8dbdca1f1345ee | |
| parent | 9128c224c65f8f2a172b22a443ccb4573c7acd90 (diff) | |
refactor(components): rewrite List component
* change `items` prop to children
* replace `kind` prop with `isHierarchical`, `isOrdered` & `isInline` props
* add `hideMarker` prop
* add `spacing` prop to control item spacing
* move lists styles to Sass placeholders to avoid repeats because of
headless WordPress
51 files changed, 889 insertions, 741 deletions
diff --git a/src/components/atoms/images/icons/svg-paths/icons-paths/arrow-icon-paths.tsx b/src/components/atoms/images/icons/svg-paths/icons-paths/arrow-icon-paths.tsx index 0dc701a..9d863a0 100644 --- a/src/components/atoms/images/icons/svg-paths/icons-paths/arrow-icon-paths.tsx +++ b/src/components/atoms/images/icons/svg-paths/icons-paths/arrow-icon-paths.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/jsx-no-literals */ import type { FC } from 'react'; -import type { Position } from '../../../../../types'; +import type { Position } from '../../../../../../types'; export type ArrowOrientation = Exclude<Position, 'center'>; diff --git a/src/components/atoms/images/logo/logo.module.scss b/src/components/atoms/images/logo/logo.module.scss index bd4ee90..beeb7ff 100644 --- a/src/components/atoms/images/logo/logo.module.scss +++ b/src/components/atoms/images/logo/logo.module.scss @@ -1,4 +1,4 @@ -@use "../../../styles/abstracts/functions" as fun; +@use "../../../../styles/abstracts/functions" as fun; .wrapper { width: var(--logo-size, fun.convert-px(100)); diff --git a/src/components/atoms/lists/list.module.scss b/src/components/atoms/lists/list.module.scss deleted file mode 100644 index cbb391e..0000000 --- a/src/components/atoms/lists/list.module.scss +++ /dev/null @@ -1,45 +0,0 @@ -@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 deleted file mode 100644 index c4f3c3b..0000000 --- a/src/components/atoms/lists/list.stories.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { List, type ListItem } from './list'; - -/** - * List - Storybook Meta - */ -export default { - title: 'Atoms/Typography/Lists', - component: List, - 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 List>; - -const Template: ComponentStory<typeof List> = (args) => <List {...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 deleted file mode 100644 index 89de922..0000000 --- a/src/components/atoms/lists/list.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/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 deleted file mode 100644 index 8fc9672..0000000 --- a/src/components/atoms/lists/list.tsx +++ /dev/null @@ -1,74 +0,0 @@ -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. - */ -export const List: FC<ListProps> = ({ - className = '', - items, - itemsClassName = '', - kind = 'unordered', -}) => { - const ListTag = kind === 'ordered' ? 'ol' : 'ul'; - const kindClass = `list--${kind}`; - const listClass = `${styles.list} ${styles[kindClass]} ${className}`; - - /** - * 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={listClass}>{getItems(items)}</ListTag>; -}; diff --git a/src/components/atoms/lists/list/index.ts b/src/components/atoms/lists/list/index.ts new file mode 100644 index 0000000..553d0ba --- /dev/null +++ b/src/components/atoms/lists/list/index.ts @@ -0,0 +1,2 @@ +export * from './list'; +export * from './list-item'; diff --git a/src/components/atoms/lists/list/list-item.tsx b/src/components/atoms/lists/list/list-item.tsx new file mode 100644 index 0000000..3438eac --- /dev/null +++ b/src/components/atoms/lists/list/list-item.tsx @@ -0,0 +1,23 @@ +import type { FC, LiHTMLAttributes } from 'react'; +import styles from './list.module.scss'; + +export type ListItemProps = LiHTMLAttributes<HTMLElement>; + +/** + * ListItem component + * + * Used it inside a `<List />` component. + */ +export const ListItem: FC<ListItemProps> = ({ + children, + className = '', + ...props +}) => { + const itemClass = `${styles.item} ${className}`; + + return ( + <li {...props} className={itemClass}> + {children} + </li> + ); +}; diff --git a/src/components/atoms/lists/list/list.module.scss b/src/components/atoms/lists/list/list.module.scss new file mode 100644 index 0000000..5a38b44 --- /dev/null +++ b/src/components/atoms/lists/list/list.module.scss @@ -0,0 +1,28 @@ +@use "../../../../styles/abstracts/placeholders"; + +.list { + &--hierarchical { + @extend %hierarchical-list; + } + + &--inline { + @extend %inline-list; + } + + &--stack { + .item { + @extend %list-item; + } + } + + &--stack#{&} { + &--ordered, + &--unordered { + @extend %regular-list; + } + } + + &--no-marker { + list-style-type: none; + } +} diff --git a/src/components/atoms/lists/list/list.stories.tsx b/src/components/atoms/lists/list/list.stories.tsx new file mode 100644 index 0000000..538947a --- /dev/null +++ b/src/components/atoms/lists/list/list.stories.tsx @@ -0,0 +1,196 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { List, type ListProps } from './list'; +import { ListItem } from './list-item'; + +/** + * List - Storybook Meta + */ +export default { + title: 'Atoms/Lists', + component: List, + args: {}, + argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the list wrapper', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta<typeof List>; + +const Template: ComponentStory<typeof List> = < + O extends boolean, + H extends boolean, +>( + args: ListProps<O, H> +) => <List {...args} />; + +/** + * List Stories - Hierarchical list + */ +export const Hierarchical = Template.bind({}); +Hierarchical.args = { + children: [ + <ListItem key="item-1"> + Item 1 + <List isHierarchical isOrdered> + <ListItem>Subitem 1</ListItem> + <ListItem>Subitem 2</ListItem> + </List> + </ListItem>, + <ListItem key="item-2"> + Item 2 + <List isHierarchical isOrdered> + <ListItem>Subitem 1</ListItem> + <ListItem> + Subitem 2 + <List isHierarchical isOrdered> + <ListItem>Nested item 1</ListItem> + <ListItem>Nested item 2</ListItem> + </List> + </ListItem> + <ListItem>Subitem 3</ListItem> + </List> + </ListItem>, + <ListItem key="item-3">Item 3</ListItem>, + ], + isHierarchical: true, +}; + +/** + * List Stories - Ordered list + */ +export const Ordered = Template.bind({}); +Ordered.args = { + children: [ + <ListItem key="item-1"> + Item 1 + <List isOrdered> + <ListItem>Subitem 1</ListItem> + <ListItem>Subitem 2</ListItem> + </List> + </ListItem>, + <ListItem key="item-2"> + Item 2 + <List isOrdered> + <ListItem>Subitem 1</ListItem> + <ListItem> + Subitem 2 + <List isOrdered> + <ListItem>Nested item 1</ListItem> + <ListItem>Nested item 2</ListItem> + </List> + </ListItem> + <ListItem>Subitem 3</ListItem> + </List> + </ListItem>, + <ListItem key="item-3">Item 3</ListItem>, + ], + isOrdered: true, +}; + +/** + * List Stories - Unordered list + */ +export const Unordered = Template.bind({}); +Unordered.args = { + children: [ + <ListItem key="item-1"> + Item 1 + <List> + <ListItem>Subitem 1</ListItem> + <ListItem>Subitem 2</ListItem> + </List> + </ListItem>, + <ListItem key="item-2"> + Item 2 + <List isOrdered> + <ListItem>Subitem 1</ListItem> + <ListItem> + Subitem 2 + <List> + <ListItem>Nested item 1</ListItem> + <ListItem>Nested item 2</ListItem> + </List> + </ListItem> + <ListItem>Subitem 3</ListItem> + </List> + </ListItem>, + <ListItem key="item-3">Item 3</ListItem>, + ], +}; + +const items = [ + { id: 'item-1', label: 'Item 1' }, + { id: 'item-2', label: 'Item 2' }, + { + id: 'item-3', + label: ( + <> + Item 3 + <List> + <ListItem>Subitem 1</ListItem> + <ListItem>Subitem 2</ListItem> + </List> + </> + ), + }, + { id: 'item-4', label: 'Item 4' }, + { id: 'item-5', label: 'Item 5' }, +]; + +/** + * List Stories - Inline and ordered list + */ +export const InlineOrdered = Template.bind({}); +InlineOrdered.args = { + children: items.map((item) => ( + <ListItem key={item.id}>{item.label}</ListItem> + )), + isInline: true, + isOrdered: true, + spacing: 'sm', +}; + +/** + * List Stories - Inline and unordered list + */ +export const InlineUnordered = Template.bind({}); +InlineUnordered.args = { + children: items.map((item) => ( + <ListItem key={item.id}>{item.label}</ListItem> + )), + isInline: true, + spacing: 'sm', +}; + +/** + * List Stories - Ordered list without marker + */ +export const OrderedHideMarker = Template.bind({}); +OrderedHideMarker.args = { + children: items.map((item) => ( + <ListItem key={item.id}>{item.label}</ListItem> + )), + hideMarker: true, + isOrdered: true, +}; + +/** + * List Stories - Unordered list without marker + */ +export const UnorderedHideMarker = Template.bind({}); +UnorderedHideMarker.args = { + children: items.map((item) => ( + <ListItem key={item.id}>{item.label}</ListItem> + )), + hideMarker: true, +}; diff --git a/src/components/atoms/lists/list/list.test.tsx b/src/components/atoms/lists/list/list.test.tsx new file mode 100644 index 0000000..d8001f5 --- /dev/null +++ b/src/components/atoms/lists/list/list.test.tsx @@ -0,0 +1,110 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { List } from './list'; +import { ListItem } from './list-item'; + +const items = [ + { id: 'item-1', label: 'Item 1' }, + { id: 'item-2', label: 'Item 2' }, + { id: 'item-3', label: 'Item 3' }, + { id: 'item-4', label: 'Item 4' }, +]; + +describe('List', () => { + it('renders a list of items', () => { + render( + <List> + {items.map((item) => ( + <ListItem key={item.id}>{item.label}</ListItem> + ))} + </List> + ); + + expect(rtlScreen.getByRole('list')).toBeInTheDocument(); + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(items.length); + }); + + it('can render an ordered list', () => { + render( + <List isOrdered> + {items.map((item) => ( + <ListItem key={item.id}>{item.label}</ListItem> + ))} + </List> + ); + + expect(rtlScreen.getByRole('list')).toHaveClass('list--ordered'); + }); + + it('can render a hierarchical list', () => { + render( + <List isHierarchical isOrdered> + {items.map((item) => ( + <ListItem key={item.id}>{item.label}</ListItem> + ))} + </List> + ); + + expect(rtlScreen.getByRole('list')).toHaveClass('list--hierarchical'); + }); + + it('can render an unordered list', () => { + render( + <List isOrdered={false}> + {items.map((item) => ( + <ListItem key={item.id}>{item.label}</ListItem> + ))} + </List> + ); + + expect(rtlScreen.getByRole('list')).toHaveClass('list--unordered'); + }); + + it('can render list items in a row', () => { + render( + <List isInline> + {items.map((item) => ( + <ListItem key={item.id}>{item.label}</ListItem> + ))} + </List> + ); + + expect(rtlScreen.getByRole('list')).toHaveClass('list--inline'); + }); + + it('can render list items as one column', () => { + render( + <List isInline={false}> + {items.map((item) => ( + <ListItem key={item.id}>{item.label}</ListItem> + ))} + </List> + ); + + expect(rtlScreen.getByRole('list')).toHaveClass('list--stack'); + }); + + it('can render a list with marker', () => { + render( + <List hideMarker={false}> + {items.map((item) => ( + <ListItem key={item.id}>{item.label}</ListItem> + ))} + </List> + ); + + expect(rtlScreen.getByRole('list')).toHaveClass('list--has-marker'); + }); + + it('can render a list without marker', () => { + render( + <List hideMarker> + {items.map((item) => ( + <ListItem key={item.id}>{item.label}</ListItem> + ))} + </List> + ); + + expect(rtlScreen.getByRole('list')).toHaveClass('list--no-marker'); + }); +}); diff --git a/src/components/atoms/lists/list/list.tsx b/src/components/atoms/lists/list/list.tsx new file mode 100644 index 0000000..6e58433 --- /dev/null +++ b/src/components/atoms/lists/list/list.tsx @@ -0,0 +1,150 @@ +import { + forwardRef, + type CSSProperties, + type HTMLAttributes, + type OlHTMLAttributes, + type ReactNode, + type ForwardedRef, +} from 'react'; +import type { Spacing } from '../../../../types'; +import styles from './list.module.scss'; + +type OrderedListProps = Omit<OlHTMLAttributes<HTMLOListElement>, 'children'>; + +type UnorderedListProps = Omit<HTMLAttributes<HTMLUListElement>, 'children'>; + +type BaseListProps<O extends boolean, H extends boolean> = O extends true + ? OrderedListProps + : H extends true + ? OrderedListProps + : UnorderedListProps; + +type AdditionalProps<O extends boolean, H extends boolean> = { + /** + * An array of list items. + */ + children: ReactNode; + /** + * Should the items marker be hidden? + * + * @default false + */ + hideMarker?: boolean; + /** + * Should the list be ordered and hierarchical? + * + * @default false + */ + isHierarchical?: H; + /** + * Should the list be inlined? + * + * @default false + */ + isInline?: boolean; + /** + * Should the list be ordered? + * + * @default false + */ + isOrdered?: O; + /** + * Define the spacing between list items. + * + * @default null + */ + spacing?: Spacing | null; +}; + +type BuildClassNameConfig<O extends boolean, H extends boolean> = Pick< + BaseListProps<O, H>, + 'className' +> & + Pick< + AdditionalProps<O, H>, + 'hideMarker' | 'isHierarchical' | 'isInline' | 'isOrdered' + >; + +const buildClassName = <O extends boolean, H extends boolean>({ + className = '', + hideMarker, + isHierarchical, + isInline, + isOrdered, +}: BuildClassNameConfig<O, H>) => { + const orderedClassName = isHierarchical + ? 'list--hierarchical' + : 'list--ordered'; + const classNames: string[] = [ + isHierarchical || isOrdered ? orderedClassName : 'list--unordered', + isInline ? 'list--inline' : 'list--stack', + hideMarker ? 'list--no-marker' : 'list--has-marker', + className, + ].map((key) => styles[key]); + + if (className) classNames.push(className); + + return classNames.join(' '); +}; + +export type ListProps<O extends boolean, H extends boolean> = BaseListProps< + O, + H +> & + AdditionalProps<O, H>; + +const ListWithRef = <O extends boolean, H extends boolean>( + { + className, + children, + hideMarker = false, + isHierarchical, + isInline = false, + isOrdered, + spacing = null, + style, + ...props + }: ListProps<O, H>, + ref: ForwardedRef< + O extends true + ? HTMLOListElement + : H extends true + ? HTMLOListElement + : HTMLUListElement + > +) => { + const itemSpacing = spacing === null ? 0 : `var(--spacing-${spacing})`; + const listClass = buildClassName({ + className, + hideMarker, + isHierarchical, + isInline, + isOrdered, + }); + const listStyles = { + ...style, + '--itemSpacing': itemSpacing, + } as CSSProperties; + + return isHierarchical || isOrdered ? ( + <ol + {...props} + className={listClass} + ref={ref as ForwardedRef<HTMLOListElement>} + style={listStyles} + > + {children} + </ol> + ) : ( + <ul {...props} className={listClass} ref={ref} style={listStyles}> + {children} + </ul> + ); +}; + +/** + * List component + * + * Render either an ordered or an unordered list. + */ +export const List = forwardRef(ListWithRef); diff --git a/src/components/molecules/nav/breadcrumb.module.scss b/src/components/molecules/nav/breadcrumb.module.scss index c72e349..6786896 100644 --- a/src/components/molecules/nav/breadcrumb.module.scss +++ b/src/components/molecules/nav/breadcrumb.module.scss @@ -1,14 +1,5 @@ @use "../../../styles/abstracts/placeholders"; -.list { - @extend %reset-ordered-list; - - display: flex; - flex-flow: row wrap; - align-items: center; - gap: var(--spacing-2xs); -} - .item { &:not(:last-of-type) { &::after { diff --git a/src/components/molecules/nav/breadcrumb.tsx b/src/components/molecules/nav/breadcrumb.tsx index 247bc2b..51f4633 100644 --- a/src/components/molecules/nav/breadcrumb.tsx +++ b/src/components/molecules/nav/breadcrumb.tsx @@ -1,9 +1,13 @@ import Script from 'next/script'; -import { FC } from 'react'; +import type { FC } from 'react'; import { useIntl } from 'react-intl'; -import { BreadcrumbList, ListItem, WithContext } from 'schema-dts'; +import type { + BreadcrumbList, + ListItem as ListItemType, + WithContext, +} from 'schema-dts'; import { settings } from '../../../utils/config'; -import { Link } from '../../atoms'; +import { Link, List, ListItem } from '../../atoms'; import styles from './breadcrumb.module.scss'; export type BreadcrumbItem = { @@ -60,29 +64,28 @@ export const Breadcrumb: FC<BreadcrumbProps> = ({ * @param {BreadcrumbItem[]} list - The breadcrumb items. * @returns {JSX.Element[]} The list items. */ - const getListItems = (list: BreadcrumbItem[]): JSX.Element[] => { - return list.map((item, index) => { + const getListItems = (list: BreadcrumbItem[]): JSX.Element[] => + list.map((item, index) => { const isLastItem = index === list.length - 1; const itemStyles = isLastItem ? `${styles.item} screen-reader-text` : styles.item; return ( - <li key={item.id} className={`${itemStyles} ${itemClassName}`}> + <ListItem key={item.id} className={`${itemStyles} ${itemClassName}`}> {isLastItem ? item.name : <Link href={item.url}>{item.name}</Link>} - </li> + </ListItem> ); }); - }; /** * Retrieve the breadcrumb list items with Schema.org format. * * @param {BreadcrumbItem[]} list - The breadcrumb items. - * @returns {ListItem[]} An array of list items using Schema.org format. + * @returns {ListItemType[]} An array of list items using Schema.org format. */ - const getSchemaItems = (list: BreadcrumbItem[]): ListItem[] => { - const schemaItems: ListItem[] = []; + const getSchemaItems = (list: BreadcrumbItem[]): ListItemType[] => { + const schemaItems: ListItemType[] = []; list.forEach((item, index) => { schemaItems.push({ @@ -118,7 +121,9 @@ export const Breadcrumb: FC<BreadcrumbProps> = ({ id: '16zl9Z', })} </span> - <ol className={styles.list}>{getListItems(items)}</ol> + <List hideMarker isInline isOrdered spacing="2xs"> + {getListItems(items)} + </List> </nav> </> ); diff --git a/src/components/molecules/nav/nav-list.module.scss b/src/components/molecules/nav/nav-list.module.scss index a6d43bc..ff99581 100644 --- a/src/components/molecules/nav/nav-list.module.scss +++ b/src/components/molecules/nav/nav-list.module.scss @@ -1,15 +1,4 @@ -@use "../../../styles/abstracts/placeholders"; - .nav { - &__list { - @extend %reset-list; - - display: flex; - flex-flow: row wrap; - gap: var(--spacing-2xs); - align-items: center; - } - &--footer & { &__item:not(:first-child) { &::before { diff --git a/src/components/molecules/nav/nav-list.tsx b/src/components/molecules/nav/nav-list.tsx index 59556ce..55c2aa9 100644 --- a/src/components/molecules/nav/nav-list.tsx +++ b/src/components/molecules/nav/nav-list.tsx @@ -1,5 +1,5 @@ import type { FC, ReactNode } from 'react'; -import { Link, Nav, NavLink, type NavProps } from '../../atoms'; +import { Link, List, ListItem, Nav, NavLink, type NavProps } from '../../atoms'; import styles from './nav-list.module.scss'; export type NavItem = { @@ -50,7 +50,6 @@ export const NavList: FC<NavListProps> = ({ }) => { const kindClass = `nav--${kind}`; const navClass = `${styles[kindClass]} ${className}`; - const listClass = `${styles.nav__list} ${listClassName}`; /** * Get the nav items. @@ -58,18 +57,20 @@ export const NavList: FC<NavListProps> = ({ */ const getItems = (): JSX.Element[] => items.map(({ id, href, label, logo }) => ( - <li key={id} className={styles.nav__item}> + <ListItem key={id} className={styles.nav__item}> {kind === 'main' ? ( <NavLink href={href} label={label} logo={logo} /> ) : ( <Link href={href}>{label}</Link> )} - </li> + </ListItem> )); return ( <Nav {...props} className={navClass}> - <ul className={listClass}>{getItems()}</ul> + <List className={listClassName} hideMarker isInline spacing="2xs"> + {getItems()} + </List> </Nav> ); }; diff --git a/src/components/molecules/nav/pagination.module.scss b/src/components/molecules/nav/pagination.module.scss index 4ffad5a..8b06a95 100644 --- a/src/components/molecules/nav/pagination.module.scss +++ b/src/components/molecules/nav/pagination.module.scss @@ -1,18 +1,10 @@ @use "../../../styles/abstracts/functions" as fun; -@use "../../../styles/abstracts/placeholders"; .wrapper { .list { - @extend %flex-list; - - align-items: stretch; justify-content: center; - position: relative; - row-gap: var(--spacing-xs); - column-gap: var(--spacing-sm); &--pages { - column-gap: var(--spacing-2xs); margin-bottom: var(--spacing-sm); } } diff --git a/src/components/molecules/nav/pagination.tsx b/src/components/molecules/nav/pagination.tsx index 27ef1ec..73517c3 100644 --- a/src/components/molecules/nav/pagination.tsx +++ b/src/components/molecules/nav/pagination.tsx @@ -1,7 +1,7 @@ /* eslint-disable max-statements */ import { type FC, Fragment, type ReactNode } from 'react'; import { useIntl } from 'react-intl'; -import { ButtonLink } from '../../atoms'; +import { ButtonLink, List, ListItem } from '../../atoms'; import styles from './pagination.module.scss'; export type PaginationProps = { @@ -140,7 +140,7 @@ export const Pagination: FC<PaginationProps> = ({ const disabledLinkClass = `${styles.link} ${styles['link--disabled']}`; return ( - <li className={styles.item}> + <ListItem className={styles.item}> {link ? ( <ButtonLink className={linkClass} kind={kind} to={link}> {body} @@ -148,7 +148,7 @@ export const Pagination: FC<PaginationProps> = ({ ) : ( <span className={disabledLinkClass}>{body}</span> )} - </li> + </ListItem> ); }; @@ -202,13 +202,15 @@ export const Pagination: FC<PaginationProps> = ({ return ( <nav {...props} className={navClass}> - <ul className={listClass}>{getPages(current, totalPages)}</ul> - <ul className={styles.list}> + <List className={listClass} hideMarker isInline spacing="2xs"> + {getPages(current, totalPages)} + </List> + <List className={styles.list} hideMarker isInline spacing="xs"> {hasPreviousPage ? getItem('previous', previousPageName, previousPageUrl) : null} {hasNextPage ? getItem('next', nextPageName, nextPageUrl) : null} - </ul> + </List> </nav> ); }; diff --git a/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx b/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx index 681d384..8ada948 100644 --- a/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx +++ b/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx @@ -1,17 +1,18 @@ -import { ChangeEvent, FC, useState } from 'react'; +/* eslint-disable max-statements */ +import { type ChangeEvent, type FC, useState, useCallback } from 'react'; import { useIntl } from 'react-intl'; import { type AckeeOptions, useLocalStorage, useUpdateAckeeOptions, } from '../../../../utils/hooks'; -import { Legend, List } from '../../../atoms'; +import { Legend, List, ListItem } from '../../../atoms'; import { Switch, - SwitchOption, - SwitchProps, + type SwitchOption, + type SwitchProps, Tooltip, - TooltipProps, + type TooltipProps, } from '../../../molecules'; export type AckeeToggleProps = Omit< @@ -85,29 +86,24 @@ export const AckeeToggle: FC<AckeeToggleProps> = ({ id: '7zDlQo', }); - const options: [SwitchOption, SwitchOption] = [ - { - id: 'ackee-full', - label: fullLabel, - value: 'full', - }, - { - id: 'ackee-partial', - label: partialLabel, - value: 'partial', - }, - ]; + const options = [ + { id: 'ackee-full' as const, label: fullLabel, value: 'full' }, + { id: 'ackee-partial' as const, label: partialLabel, value: 'partial' }, + ] satisfies [SwitchOption, SwitchOption]; - const updateSetting = (e: ChangeEvent<HTMLInputElement>) => { - setValue(e.target.value === 'full' ? 'full' : 'partial'); - }; + const updateSetting = useCallback( + (e: ChangeEvent<HTMLInputElement>) => { + setValue(e.target.value === 'full' ? 'full' : 'partial'); + }, + [setValue] + ); - const closeTooltip = () => { + const closeTooltip = useCallback(() => { setIsTooltipOpened(false); - }; - const toggleTooltip = () => { + }, []); + const toggleTooltip = useCallback(() => { setIsTooltipOpened((prev) => !prev); - }; + }, []); return ( <Switch @@ -125,12 +121,13 @@ export const AckeeToggle: FC<AckeeToggleProps> = ({ onClickOutside={closeTooltip} onToggle={toggleTooltip} > - <List - items={[ - { id: 'partial', value: tooltipPartial }, - { id: 'full', value: tooltipFull }, - ]} - /> + <List> + {options.map((option) => ( + <ListItem key={option.id}> + {option.id === 'ackee-full' ? tooltipFull : tooltipPartial} + </ListItem> + ))} + </List> </Tooltip> } value={value} diff --git a/src/components/organisms/images/gallery.module.scss b/src/components/organisms/images/gallery.module.scss index f4faa63..31960a4 100644 --- a/src/components/organisms/images/gallery.module.scss +++ b/src/components/organisms/images/gallery.module.scss @@ -2,8 +2,6 @@ @use "../../../styles/abstracts/placeholders"; .wrapper { - @extend %reset-list; - display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--spacing-sm); diff --git a/src/components/organisms/images/gallery.stories.tsx b/src/components/organisms/images/gallery.stories.tsx index 8b68777..5005ed8 100644 --- a/src/components/organisms/images/gallery.stories.tsx +++ b/src/components/organisms/images/gallery.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { ResponsiveImage } from '../../molecules'; import { Gallery } from './gallery'; @@ -37,7 +37,7 @@ export default { const image = { alt: 'Modi provident omnis', height: 480, - src: 'http://placeimg.com/640/480/fashion', + src: 'http://picsum.photos/640/480', width: 640, }; diff --git a/src/components/organisms/images/gallery.tsx b/src/components/organisms/images/gallery.tsx index ae773f6..b35acfe 100644 --- a/src/components/organisms/images/gallery.tsx +++ b/src/components/organisms/images/gallery.tsx @@ -1,7 +1,9 @@ -import { Children, FC, ReactElement } from 'react'; -import { type ResponsiveImageProps } from '../../molecules'; +import { Children, type FC, type ReactElement } from 'react'; +import { List, ListItem } from '../../atoms'; +import type { ResponsiveImageProps } from '../../molecules'; import styles from './gallery.module.scss'; +// eslint-disable-next-line @typescript-eslint/no-magic-numbers export type GalleryColumn = 2 | 3 | 4; export type GalleryProps = { @@ -24,10 +26,10 @@ export const Gallery: FC<GalleryProps> = ({ children, columns }) => { const columnsClass = `wrapper--${columns}-columns`; return ( - <ul className={`${styles.wrapper} ${styles[columnsClass]}`}> - {Children.map(children, (child) => { - return <li className={styles.item}>{child}</li>; - })} - </ul> + <List className={`${styles.wrapper} ${styles[columnsClass]}`} hideMarker> + {Children.map(children, (child) => ( + <ListItem className={styles.item}>{child}</ListItem> + ))} + </List> ); }; diff --git a/src/components/organisms/layout/cards-list.module.scss b/src/components/organisms/layout/cards-list.module.scss index ff79f33..8b18c08 100644 --- a/src/components/organisms/layout/cards-list.module.scss +++ b/src/components/organisms/layout/cards-list.module.scss @@ -17,14 +17,6 @@ gap: var(--spacing-lg); } } - - &--ordered { - @extend %reset-ordered-list; - } - - &--unordered { - @extend %reset-list; - } } .card { diff --git a/src/components/organisms/layout/cards-list.stories.tsx b/src/components/organisms/layout/cards-list.stories.tsx index b5f697a..1b5051f 100644 --- a/src/components/organisms/layout/cards-list.stories.tsx +++ b/src/components/organisms/layout/cards-list.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { CardsList as CardsListComponent, type CardsListItem, @@ -90,7 +90,7 @@ const items: CardsListItem[] = [ id: 'card-1', cover: { alt: 'card 1 picture', - src: 'http://placeimg.com/640/480', + src: 'http://picsum.photos/640/480', width: 640, height: 480, }, @@ -103,7 +103,7 @@ const items: CardsListItem[] = [ id: 'card-2', cover: { alt: 'card 2 picture', - src: 'http://placeimg.com/640/480', + src: 'http://picsum.photos/640/480', width: 640, height: 480, }, @@ -116,7 +116,7 @@ const items: CardsListItem[] = [ id: 'card-3', cover: { alt: 'card 3 picture', - src: 'http://placeimg.com/640/480', + src: 'http://picsum.photos/640/480', width: 640, height: 480, }, diff --git a/src/components/organisms/layout/cards-list.tsx b/src/components/organisms/layout/cards-list.tsx index e3d1156..29add3b 100644 --- a/src/components/organisms/layout/cards-list.tsx +++ b/src/components/organisms/layout/cards-list.tsx @@ -1,5 +1,5 @@ -import { FC } from 'react'; -import { List, type ListItem, type ListProps } from '../../atoms'; +import type { FC } from 'react'; +import { List, ListItem } from '../../atoms'; import { Card, type CardProps } from '../../molecules'; import styles from './cards-list.module.scss'; @@ -10,17 +10,22 @@ export type CardsListItem = Omit<CardProps, 'className' | 'titleLevel'> & { id: string; }; -export type CardsListProps = Pick<CardProps, 'titleLevel'> & - Pick<ListProps, 'kind'> & { - /** - * Set additional classnames to the list wrapper. - */ - className?: string; - /** - * The cards data. - */ - items: CardsListItem[]; - }; +export type CardsListProps = Pick<CardProps, 'titleLevel'> & { + /** + * Set additional classnames to the list wrapper. + */ + className?: string; + /** + * Should the cards list be ordered? + * + * @default false + */ + isOrdered?: boolean; + /** + * The cards data. + */ + items: CardsListItem[]; +}; /** * CardsList component @@ -29,40 +34,30 @@ export type CardsListProps = Pick<CardProps, 'titleLevel'> & */ export const CardsList: FC<CardsListProps> = ({ className = '', + isOrdered = false, items, - kind = 'unordered', titleLevel, }) => { - const kindModifier = `wrapper--${kind}`; + const kindModifier = `wrapper--${isOrdered ? 'ordered' : 'unordered'}`; - /** - * Format the cards data to be used by the List component. - * - * @param {CardsListItem[]} cards - An array of card data. - * @returns {ListItem[]} The formatted cards data. - */ - const getCards = (cards: CardsListItem[]): ListItem[] => { - return cards.map(({ id, ...card }) => { - return { - id, - value: ( + return ( + <List + className={`${styles.wrapper} ${styles[kindModifier]} ${className}`} + hideMarker + isInline + isOrdered={isOrdered} + > + {items.map(({ id, ...item }) => ( + <ListItem key={id}> <Card - {...card} + {...item} className={styles.card} key={id} id={id} titleLevel={titleLevel} /> - ), - }; - }); - }; - - return ( - <List - className={`${styles.wrapper} ${styles[kindModifier]} ${className}`} - kind="flex" - items={getCards(items)} - /> + </ListItem> + ))} + </List> ); }; diff --git a/src/components/organisms/layout/comments-list.fixture.tsx b/src/components/organisms/layout/comments-list.fixture.ts index 5842129..30a4f11 100644 --- a/src/components/organisms/layout/comments-list.fixture.tsx +++ b/src/components/organisms/layout/comments-list.fixture.ts @@ -1,4 +1,4 @@ -import { SingleComment } from '../../../types/app'; +import type { SingleComment } from '../../../types/app'; export const comments: SingleComment[] = [ { @@ -11,7 +11,7 @@ export const comments: SingleComment[] = [ avatar: { alt: 'Author 1 avatar', height: 480, - src: 'http://placeimg.com/640/480', + src: 'http://picsum.photos/640/480', width: 640, }, name: 'Author 1', @@ -24,14 +24,14 @@ export const comments: SingleComment[] = [ { approved: true, content: - 'Sit sed error quasi voluptatem velit voluptas aut. Aut debitis eveniet. Praesentium dolores quia voluptate vero quis dicta quasi vel. Aut voluptas accusantium ut aut quidem consectetur itaque laboriosam occaecati.', + 'Sit sed error quasi voluptatem velit voluptas aut. Aut debitis eveniet. Praesentium dolores quia voluptate vero quis dicta quasi vel. Aut voluptas accusantium ut aut quidem consectetur itaque laboriosam rerum.', id: 2, meta: { author: { avatar: { alt: 'Author 2 avatar', height: 480, - src: 'http://placeimg.com/640/480', + src: 'http://picsum.photos/640/480', width: 640, }, name: 'Author 2', @@ -51,7 +51,7 @@ export const comments: SingleComment[] = [ avatar: { alt: 'Author 4 avatar', height: 480, - src: 'http://placeimg.com/640/480', + src: 'http://picsum.photos/640/480', width: 640, }, name: 'Author 4', @@ -71,7 +71,7 @@ export const comments: SingleComment[] = [ avatar: { alt: 'Author 1 avatar', height: 480, - src: 'http://placeimg.com/640/480', + src: 'http://picsum.photos/640/480', width: 640, }, name: 'Author 1', @@ -93,7 +93,7 @@ export const comments: SingleComment[] = [ avatar: { alt: 'Author 3', height: 480, - src: 'http://placeimg.com/640/480', + src: 'http://picsum.photos/640/480', width: 640, }, name: 'Author 3', diff --git a/src/components/organisms/layout/comments-list.module.scss b/src/components/organisms/layout/comments-list.module.scss deleted file mode 100644 index f7a0cf0..0000000 --- a/src/components/organisms/layout/comments-list.module.scss +++ /dev/null @@ -1,16 +0,0 @@ -@use "../../../styles/abstracts/placeholders"; - -.list { - @extend %reset-ordered-list; - - & & { - margin: var(--spacing-sm) 0; - padding-left: var(--spacing-sm); - } -} - -.item { - &:not(:last-child) { - margin-bottom: var(--spacing-sm); - } -} diff --git a/src/components/organisms/layout/comments-list.tsx b/src/components/organisms/layout/comments-list.tsx index 1ce0cf5..103bfb4 100644 --- a/src/components/organisms/layout/comments-list.tsx +++ b/src/components/organisms/layout/comments-list.tsx @@ -1,7 +1,12 @@ -import { FC } from 'react'; -import { type SingleComment } from '../../../types'; +import type { FC } from 'react'; +import type { SingleComment } from '../../../types'; +import { List, ListItem } from '../../atoms'; + +// eslint-disable-next-line @typescript-eslint/no-shadow import { Comment, type CommentProps } from './comment'; -import styles from './comments-list.module.scss'; + +// eslint-disable-next-line @typescript-eslint/no-magic-numbers +export type CommentsListDepth = 0 | 1 | 2 | 3 | 4; export type CommentsListProps = Pick<CommentProps, 'Notice' | 'saveComment'> & { /** @@ -11,7 +16,7 @@ export type CommentsListProps = Pick<CommentProps, 'Notice' | 'saveComment'> & { /** * The maximum depth. Use `0` to not display nested comments. */ - depth: 0 | 1 | 2 | 3 | 4; + depth: CommentsListDepth; }; /** @@ -38,19 +43,25 @@ export const CommentsList: FC<CommentsListProps> = ({ const isLastLevel = startLevel === depth; return commentsList.map(({ replies, ...comment }) => ( - <li key={comment.id} className={styles.item}> + <ListItem key={comment.id}> <Comment canReply={!isLastLevel} Notice={Notice} saveComment={saveComment} {...comment} /> - {replies && !isLastLevel && ( - <ol className={styles.list}>{getItems(replies, startLevel + 1)}</ol> - )} - </li> + {replies.length && !isLastLevel ? ( + <List hideMarker isOrdered spacing="sm"> + {getItems(replies, startLevel + 1)} + </List> + ) : null} + </ListItem> )); }; - return <ol className={styles.list}>{getItems(comments, 0)}</ol>; + return ( + <List hideMarker isOrdered spacing="sm"> + {getItems(comments, 0)} + </List> + ); }; diff --git a/src/components/organisms/layout/posts-list.fixture.tsx b/src/components/organisms/layout/posts-list.fixture.ts index 97a746f..6109411 100644 --- a/src/components/organisms/layout/posts-list.fixture.tsx +++ b/src/components/organisms/layout/posts-list.fixture.ts @@ -1,4 +1,4 @@ -import { type Post } from './posts-list'; +import type { Post } from './posts-list'; export const introPost1 = 'Esse et voluptas sapiente modi impedit unde et. Ducimus nulla ea impedit sit placeat nihil assumenda. Rem est fugiat amet quo hic. Corrupti fuga quod animi autem dolorem ullam corrupti vel aut.'; @@ -12,7 +12,7 @@ export const introPost3 = export const cover = { alt: 'cover', height: 480, - src: 'http://placeimg.com/640/480', + src: 'http://picsum.photos/640/480', width: 640, }; diff --git a/src/components/organisms/layout/posts-list.module.scss b/src/components/organisms/layout/posts-list.module.scss index 49993da..759902a 100644 --- a/src/components/organisms/layout/posts-list.module.scss +++ b/src/components/organisms/layout/posts-list.module.scss @@ -18,14 +18,8 @@ } .list { - @extend %reset-ordered-list; - .item { border-bottom: fun.convert-px(1) solid var(--color-border); - - &:not(:last-child) { - margin-bottom: var(--spacing-md); - } } } diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx index 86c3d12..cde81e6 100644 --- a/src/components/organisms/layout/posts-list.tsx +++ b/src/components/organisms/layout/posts-list.tsx @@ -8,6 +8,8 @@ import { type HeadingLevel, ProgressBar, Spinner, + List, + ListItem, } from '../../atoms'; import { Pagination, type PaginationProps } from '../../molecules'; import { NoResults, type NoResultsProps } from './no-results'; @@ -115,25 +117,28 @@ export const PostsList: FC<PostsListProps> = ({ allPosts: Post[], headingLevel: HeadingLevel = 2 ): JSX.Element => ( - <ol + <List aria-busy={isLoading} aria-describedby={progressBarId} className={styles.list} + hideMarker + isOrdered ref={listRef} + spacing="md" > {allPosts.map(({ id, ...post }) => ( <Fragment key={id}> - <li className={styles.item}> + <ListItem className={styles.item}> <Summary {...post} titleLevel={headingLevel} /> - </li> + </ListItem> {id === lastPostId && ( - <li> + <ListItem> <span ref={lastPostRef} tabIndex={-1} /> - </li> + </ListItem> )} </Fragment> ))} - </ol> + </List> ); /** diff --git a/src/components/organisms/widgets/links-list-widget.module.scss b/src/components/organisms/widgets/links-list-widget.module.scss index 26f1549..4efc2d4 100644 --- a/src/components/organisms/widgets/links-list-widget.module.scss +++ b/src/components/organisms/widgets/links-list-widget.module.scss @@ -4,6 +4,8 @@ .widget { .list { .list { + padding: 0; + > *:first-child { border-top: fun.convert-px(1) solid var(--color-primary); } @@ -35,8 +37,6 @@ } &--ordered { - @extend %reset-ordered-list; - counter-reset: link; .list__link { @@ -50,10 +50,6 @@ } } - &--unordered { - @extend %reset-list; - } - &__item { &:not(:last-child) { border-bottom: fun.convert-px(1) solid var(--color-primary); diff --git a/src/components/organisms/widgets/links-list-widget.stories.tsx b/src/components/organisms/widgets/links-list-widget.stories.tsx index 3f62502..2180de4 100644 --- a/src/components/organisms/widgets/links-list-widget.stories.tsx +++ b/src/components/organisms/widgets/links-list-widget.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { LinksListWidget } from './links-list-widget'; /** @@ -8,7 +8,7 @@ export default { title: 'Organisms/Widgets/LinksList', component: LinksListWidget, args: { - kind: 'unordered', + isOrdered: false, }, argTypes: { className: { @@ -32,21 +32,6 @@ export default { value: {}, }, }, - kind: { - control: { - type: 'select', - }, - description: 'The list kind: either ordered or unordered.', - options: ['ordered', 'unordered'], - table: { - category: 'Options', - defaultValue: { summary: 'unordered' }, - }, - type: { - name: 'string', - required: false, - }, - }, level: { control: { type: 'number', @@ -105,7 +90,6 @@ const items = [ export const Unordered = Template.bind({}); Unordered.args = { items, - kind: 'unordered', level: 2, title: 'A list of links', }; @@ -115,8 +99,8 @@ Unordered.args = { */ export const Ordered = Template.bind({}); Ordered.args = { + isOrdered: true, items, - kind: 'ordered', level: 2, title: 'A list of links', }; diff --git a/src/components/organisms/widgets/links-list-widget.test.tsx b/src/components/organisms/widgets/links-list-widget.test.tsx index 2d36eb5..6323e19 100644 --- a/src/components/organisms/widgets/links-list-widget.test.tsx +++ b/src/components/organisms/widgets/links-list-widget.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; import { LinksListWidget } from './links-list-widget'; const title = 'Voluptatem minus autem'; @@ -14,20 +14,19 @@ describe('LinksListWidget', () => { it('renders a widget title', () => { render(<LinksListWidget items={items} title={title} level={2} />); expect( - screen.getByRole('heading', { level: 2, name: new RegExp(title, 'i') }) + rtlScreen.getByRole('heading', { level: 2, name: new RegExp(title, 'i') }) ).toBeInTheDocument(); }); it('renders the correct number of items', () => { render(<LinksListWidget items={items} title={title} level={2} />); - expect(screen.getAllByRole('listitem')).toHaveLength(items.length); + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(items.length); }); it('renders some links', () => { render(<LinksListWidget items={items} title={title} level={2} />); - expect(screen.getByRole('link', { name: items[0].name })).toHaveAttribute( - 'href', - items[0].url - ); + expect( + rtlScreen.getByRole('link', { name: items[0].name }) + ).toHaveAttribute('href', items[0].url); }); }); diff --git a/src/components/organisms/widgets/links-list-widget.tsx b/src/components/organisms/widgets/links-list-widget.tsx index df8430d..8f71efd 100644 --- a/src/components/organisms/widgets/links-list-widget.tsx +++ b/src/components/organisms/widgets/links-list-widget.tsx @@ -1,6 +1,6 @@ -import { FC } from 'react'; +import type { FC } from 'react'; import { slugify } from '../../../utils/helpers'; -import { Link, List, type ListItem, type ListProps } from '../../atoms'; +import { Link, List, ListItem } from '../../atoms'; import { Widget, type WidgetProps } from '../../molecules'; import styles from './links-list-widget.module.scss'; @@ -19,13 +19,19 @@ export type LinksListItems = { url: string; }; -export type LinksListWidgetProps = Pick<WidgetProps, 'level' | 'title'> & - Pick<ListProps, 'className' | 'kind'> & { - /** - * An array of name/url couple. - */ - items: LinksListItems[]; - }; +export type LinksListWidgetProps = Pick<WidgetProps, 'level' | 'title'> & { + className?: string; + /** + * Should the links be ordered? + * + * @default false + */ + isOrdered?: boolean; + /** + * An array of name/url couple. + */ + items: LinksListItems[]; +}; /** * LinksListWidget component @@ -34,11 +40,11 @@ export type LinksListWidgetProps = Pick<WidgetProps, 'level' | 'title'> & */ export const LinksListWidget: FC<LinksListWidgetProps> = ({ className = '', + isOrdered = false, items, - kind = 'unordered', ...props }) => { - const listKindClass = `list--${kind}`; + const listKindClass = `list--${isOrdered ? 'ordered' : 'unordered'}`; /** * Format the widget data to be used as List items. @@ -46,19 +52,23 @@ export const LinksListWidget: FC<LinksListWidgetProps> = ({ * @param {LinksListItems[]} data - The widget data. * @returns {ListItem[]} The list items data. */ - const getListItems = (data: LinksListItems[]): ListItem[] => { - return data.map((item) => { - return { - id: slugify(item.name), - child: item.child && getListItems(item.child), - value: ( - <Link href={item.url} className={styles.list__link}> - {item.name} - </Link> - ), - }; - }); - }; + const getListItems = (data: LinksListItems[]) => + data.map((item) => ( + <ListItem className={styles.list__item} key={slugify(item.name)}> + <Link className={styles.list__link} href={item.url}> + {item.name} + </Link> + {item.child?.length ? ( + <List + className={`${styles.list} ${styles[listKindClass]} ${className}`} + hideMarker + isOrdered={isOrdered} + > + {getListItems(item.child)} + </List> + ) : null} + </ListItem> + )); return ( <Widget @@ -70,10 +80,11 @@ export const LinksListWidget: FC<LinksListWidgetProps> = ({ > <List className={`${styles.list} ${styles[listKindClass]} ${className}`} - items={getListItems(items)} - itemsClassName={styles.list__item} - kind={kind} - /> + hideMarker + isOrdered={isOrdered} + > + {getListItems(items)} + </List> </Widget> ); }; diff --git a/src/components/organisms/widgets/social-media.module.scss b/src/components/organisms/widgets/social-media.module.scss index b7191be..1cd3f57 100644 --- a/src/components/organisms/widgets/social-media.module.scss +++ b/src/components/organisms/widgets/social-media.module.scss @@ -1,10 +1,5 @@ @use "../../../styles/abstracts/placeholders"; .list { - @extend %reset-list; - - display: flex; - flex-flow: row wrap; - gap: var(--spacing-xs); padding: 0 var(--spacing-2xs); } diff --git a/src/components/organisms/widgets/social-media.tsx b/src/components/organisms/widgets/social-media.tsx index 40513f8..f9dea58 100644 --- a/src/components/organisms/widgets/social-media.tsx +++ b/src/components/organisms/widgets/social-media.tsx @@ -1,5 +1,5 @@ -import { FC } from 'react'; -import { SocialLink, type SocialLinkProps } from '../../atoms'; +import type { FC } from 'react'; +import { List, ListItem, SocialLink, type SocialLinkProps } from '../../atoms'; import { Widget, type WidgetProps } from '../../molecules'; import styles from './social-media.module.scss'; @@ -21,17 +21,18 @@ export const SocialMedia: FC<SocialMediaProps> = ({ media, ...props }) => { * @param {SocialMedia[]} links - An array of social media name and url. * @returns {JSX.Element[]} The social links. */ - const getItems = (links: Media[]): JSX.Element[] => { - return links.map((link, index) => ( - <li key={`media-${index}`}> + const getItems = (links: Media[]): JSX.Element[] => + links.map((link, index) => ( + <ListItem key={`media-${index}`}> <SocialLink name={link.name} url={link.url} /> - </li> + </ListItem> )); - }; return ( <Widget expanded={true} {...props}> - <ul className={styles.list}>{getItems(media)}</ul> + <List className={styles.list} hideMarker isInline spacing="xs"> + {getItems(media)} + </List> </Widget> ); }; diff --git a/src/components/organisms/widgets/table-of-contents.stories.tsx b/src/components/organisms/widgets/table-of-contents.stories.tsx index 3078e35..d464715 100644 --- a/src/components/organisms/widgets/table-of-contents.stories.tsx +++ b/src/components/organisms/widgets/table-of-contents.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { TableOfContents as ToCWidget } from './table-of-contents'; /** @@ -26,7 +26,8 @@ const Template: ComponentStory<typeof ToCWidget> = (args) => ( <ToCWidget {...args} /> ); -const GetWrapper = () => { +/* eslint-disable max-statements */ +const getWrapper = () => { const wrapper = document.createElement('div'); const firstTitle = document.createElement('h2'); const firstParagraph = document.createElement('p'); @@ -50,5 +51,5 @@ const GetWrapper = () => { */ export const TableOfContents = Template.bind({}); TableOfContents.args = { - wrapper: GetWrapper(), + wrapper: getWrapper(), }; diff --git a/src/components/organisms/widgets/table-of-contents.tsx b/src/components/organisms/widgets/table-of-contents.tsx index 0e8789a..e67b495 100644 --- a/src/components/organisms/widgets/table-of-contents.tsx +++ b/src/components/organisms/widgets/table-of-contents.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import type { FC } from 'react'; import { useIntl } from 'react-intl'; import { useHeadingsTree, type Heading } from '../../../utils/hooks'; import { type LinksListItems, LinksListWidget } from './links-list-widget'; @@ -31,21 +31,20 @@ export const TableOfContents: FC<TableOfContentsProps> = ({ wrapper }) => { * @param {Heading[]} tree - The headings tree. * @returns {LinksListItems[]} The list items. */ - const getItems = (tree: Heading[]): LinksListItems[] => { - return tree.map((heading) => { + const getItems = (tree: Heading[]): LinksListItems[] => + tree.map((heading) => { return { name: heading.title, url: `#${heading.id}`, child: getItems(heading.children), }; }); - }; return ( <LinksListWidget className={styles.list} + isOrdered items={getItems(headingsTree)} - kind="ordered" level={2} title={title} /> diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx index 3910d61..5882ff6 100644 --- a/src/pages/cv.tsx +++ b/src/pages/cv.tsx @@ -19,6 +19,7 @@ import { PageLayout, SocialMedia, type MetaData, + ListItem, } from '../components'; import CVContent, { data, meta } from '../content/pages/cv.mdx'; import styles from '../styles/pages/cv.module.scss'; @@ -96,6 +97,24 @@ const H6 = ({ </Heading> ); +const OrderedList = ({ + children, + ...props +}: HTMLAttributes<HTMLUListElement>) => ( + <List {...props} isOrdered spacing="2xs"> + {children} + </List> +); + +const UnorderedList = ({ + children, + ...props +}: HTMLAttributes<HTMLUListElement>) => ( + <List {...props} spacing="2xs"> + {children} + </List> +); + const components: MDXComponents = { a: ExternalLink, h1: H1, @@ -104,8 +123,10 @@ const components: MDXComponents = { h4: H4, h5: H5, h6: H6, + li: ListItem, Link, - List, + ol: OrderedList, + ul: UnorderedList, }; /** diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 662ae81..1f1c9f3 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -14,7 +14,7 @@ import { getLayout, Icon, List, - type ListItem, + ListItem, ResponsiveImage, Section, type SectionProps, @@ -90,10 +90,10 @@ const H6 = ({ */ const CodingLinks: FC = () => { const intl = useIntl(); - const links: ListItem[] = [ - { - id: 'web-development', - value: ( + + return ( + <List className={styles.list} hideMarker isInline spacing="sm"> + <ListItem> <ButtonLink to={ROUTES.THEMATICS.WEB_DEV}> {intl.formatMessage({ defaultMessage: 'Web development', @@ -101,11 +101,8 @@ const CodingLinks: FC = () => { id: 'vkF/RP', })} </ButtonLink> - ), - }, - { - id: 'projects', - value: ( + </ListItem> + <ListItem> <ButtonLink to={ROUTES.PROJECTS}> {intl.formatMessage({ defaultMessage: 'Projects', @@ -113,12 +110,9 @@ const CodingLinks: FC = () => { id: 'N44SOc', })} </ButtonLink> - ), - }, - ]; - - // eslint-disable-next-line react/jsx-no-literals -- Kind config allowed - return <List kind="flex" items={links} className={styles.list} />; + </ListItem> + </List> + ); }; /** @@ -132,10 +126,10 @@ const ColdarkRepos: FC = () => { github: 'https://github.com/ArmandPhilippot/coldark', gitlab: 'https://gitlab.com/ArmandPhilippot/coldark', }; - const links: ListItem[] = [ - { - id: 'coldark-github', - value: ( + + return ( + <List className={styles.list} hideMarker isInline spacing="sm"> + <ListItem> <ButtonLink isExternal to={repo.github}> {intl.formatMessage({ defaultMessage: 'Github', @@ -143,11 +137,8 @@ const ColdarkRepos: FC = () => { id: '3f3PzH', })} </ButtonLink> - ), - }, - { - id: 'coldark-gitlab', - value: ( + </ListItem> + <ListItem> <ButtonLink isExternal to={repo.gitlab}> {intl.formatMessage({ defaultMessage: 'Gitlab', @@ -155,12 +146,9 @@ const ColdarkRepos: FC = () => { id: '7AnwZ7', })} </ButtonLink> - ), - }, - ]; - - // eslint-disable-next-line react/jsx-no-literals -- Kind config allowed - return <List kind="flex" items={links} className={styles.list} />; + </ListItem> + </List> + ); }; /** @@ -170,10 +158,10 @@ const ColdarkRepos: FC = () => { */ const LibreLinks: FC = () => { const intl = useIntl(); - const links: ListItem[] = [ - { - id: 'free', - value: ( + + return ( + <List className={styles.list} hideMarker isInline spacing="sm"> + <ListItem> <ButtonLink to={ROUTES.THEMATICS.FREE}> {intl.formatMessage({ defaultMessage: 'Free', @@ -181,11 +169,8 @@ const LibreLinks: FC = () => { id: 'w8GrOf', })} </ButtonLink> - ), - }, - { - id: 'linux', - value: ( + </ListItem> + <ListItem> <ButtonLink to={ROUTES.THEMATICS.LINUX}> {intl.formatMessage({ defaultMessage: 'Linux', @@ -193,12 +178,9 @@ const LibreLinks: FC = () => { id: 'jASD7k', })} </ButtonLink> - ), - }, - ]; - - // eslint-disable-next-line react/jsx-no-literals -- Kind config allowed - return <List kind="flex" items={links} className={styles.list} />; + </ListItem> + </List> + ); }; /** @@ -209,10 +191,10 @@ const LibreLinks: FC = () => { const ShaarliLink: FC = () => { const intl = useIntl(); const shaarliUrl = PERSONAL_LINKS.SHAARLI; - const links: ListItem[] = [ - { - id: 'shaarli', - value: ( + + return ( + <List className={styles.list} hideMarker isInline spacing="sm"> + <ListItem> <ButtonLink isExternal to={shaarliUrl}> {intl.formatMessage({ defaultMessage: 'Shaarli', @@ -220,12 +202,9 @@ const ShaarliLink: FC = () => { id: 'i5L19t', })} </ButtonLink> - ), - }, - ]; - - // eslint-disable-next-line react/jsx-no-literals -- Kind config allowed - return <List kind="flex" items={links} className={styles.list} />; + </ListItem> + </List> + ); }; /** @@ -235,10 +214,10 @@ const ShaarliLink: FC = () => { */ const MoreLinks: FC = () => { const intl = useIntl(); - const links: ListItem[] = [ - { - id: 'contact-me', - value: ( + + return ( + <List className={styles.list} hideMarker isInline spacing="sm"> + <ListItem> <ButtonLink to={ROUTES.CONTACT}> <Icon aria-hidden={true} shape="envelop" /> {intl.formatMessage({ @@ -247,11 +226,8 @@ const MoreLinks: FC = () => { id: 'sO/Iwj', })} </ButtonLink> - ), - }, - { - id: 'rss-feed', - value: ( + </ListItem> + <ListItem> <ButtonLink to={ROUTES.RSS}> <Icon aria-hidden={true} shape="feed" /> {intl.formatMessage({ @@ -260,12 +236,9 @@ const MoreLinks: FC = () => { id: 'T4YA64', })} </ButtonLink> - ), - }, - ]; - - // eslint-disable-next-line react/jsx-no-literals -- Kind config allowed - return <List kind="flex" items={links} className={styles.list} />; + </ListItem> + </List> + ); }; const StyledColumns = (props: ColumnsProps) => ( diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx index 717ae13..89891b3 100644 --- a/src/pages/projets/[slug].tsx +++ b/src/pages/projets/[slug].tsx @@ -23,6 +23,8 @@ import { Spinner, type MetaData, Heading, + List, + ListItem, } from '../../components'; import styles from '../../styles/pages/project.module.scss'; import type { NextPageWithLayout, ProjectPreview, Repos } from '../../types'; @@ -99,6 +101,24 @@ const H6 = ({ </Heading> ); +const OrderedList = ({ + children, + ...props +}: HTMLAttributes<HTMLUListElement>) => ( + <List {...props} isOrdered spacing="2xs"> + {children} + </List> +); + +const UnorderedList = ({ + children, + ...props +}: HTMLAttributes<HTMLUListElement>) => ( + <List {...props} spacing="2xs"> + {children} + </List> +); + const components: MDXComponents = { Code, Gallery, @@ -109,7 +129,10 @@ const components: MDXComponents = { h5: H5, h6: H6, Image: BorderedImage, + li: ListItem, Link, + ol: OrderedList, + ul: UnorderedList, }; type ProjectPageProps = { diff --git a/src/styles/abstracts/_placeholders.scss b/src/styles/abstracts/_placeholders.scss index 76fdbd6..c978f96 100644 --- a/src/styles/abstracts/_placeholders.scss +++ b/src/styles/abstracts/_placeholders.scss @@ -3,4 +3,4 @@ @forward "./placeholders/clearfix"; @forward "./placeholders/headings"; @forward "./placeholders/layout"; -@forward "./placeholders/list"; +@forward "./placeholders/lists"; diff --git a/src/styles/abstracts/_variables.scss b/src/styles/abstracts/_variables.scss index 49e8e26..c8ce857 100644 --- a/src/styles/abstracts/_variables.scss +++ b/src/styles/abstracts/_variables.scss @@ -164,7 +164,7 @@ $icon-sizes: (); $spacing_ratio: get-ratio("golden-number"); $spacing_base: $spacing_ratio * 1rem; -$spacing_levels: "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl"; +$spacing_levels: "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl"; // We start with 2xs spacing, so it needs to be less than the base. $spacing_current: math.div($spacing_base, $spacing_ratio * 3); diff --git a/src/styles/abstracts/placeholders/_list.scss b/src/styles/abstracts/placeholders/_list.scss deleted file mode 100644 index 85e8386..0000000 --- a/src/styles/abstracts/placeholders/_list.scss +++ /dev/null @@ -1,32 +0,0 @@ -/// List Reset -%reset-list { - list-style-type: none; - margin: 0; - padding: 0; - - li { - margin-bottom: 0; - } -} - -/// Ordered List Reset -%reset-ordered-list { - @extend %reset-list; - - li { - display: list-item; - counter-increment: none; - - &::before { - display: none; - } - } -} - -/// Display an inline list with flexbox -%flex-list { - @extend %reset-list; - - display: flex; - flex-flow: row wrap; -} diff --git a/src/styles/abstracts/placeholders/_lists.scss b/src/styles/abstracts/placeholders/_lists.scss new file mode 100644 index 0000000..8a6b1d9 --- /dev/null +++ b/src/styles/abstracts/placeholders/_lists.scss @@ -0,0 +1,53 @@ +%reset-list { + margin: 0; + padding: 0; + + ::marker { + color: var(--color-primary-dark); + } +} + +%regular-list { + @extend %reset-list; + + list-style-position: inside; + + ul, + ol { + margin-block-start: var(--itemSpacing); + padding-inline-start: var(--spacing-sm); + } +} + +%hierarchical-list { + @extend %reset-list; + + counter-reset: item; + + > li { + display: table; + counter-increment: item; + + &::before { + content: counters(item, ".") ". "; + display: table-cell; + padding-inline-end: var(--spacing-2xs); + color: var(--color-secondary); + } + } +} + +%inline-list { + @extend %reset-list; + + display: flex; + flex-flow: row wrap; + gap: var(--itemSpacing, 0); + list-style-position: inside; +} + +%list-item { + &:not(:last-child) { + margin-block-end: var(--itemSpacing); + } +} diff --git a/src/styles/base/_typography.scss b/src/styles/base/_typography.scss index 11b506f..c98533e 100644 --- a/src/styles/base/_typography.scss +++ b/src/styles/base/_typography.scss @@ -9,67 +9,6 @@ small { font-size: var(--font-size-sm); } -ol { - list-style-type: none; - counter-reset: li; - padding: 0; - - > li { - display: table; - counter-increment: li; - - &::before { - content: counters(li, ".") ". "; - color: var(--color-secondary); - display: table-cell; - padding-right: var(--spacing-2xs); - } - } - - li ol > li::before { - content: counters(li, ".") ". "; - } -} - -ul, -ol { - li { - margin-bottom: var(--spacing-2xs); - - &:last-child { - margin-bottom: 0; - } - } - - ::marker { - color: var(--color-primary-dark); - } -} - -ul { - padding-left: var(--spacing-sm); -} - -dl { - display: flex; - flex-flow: row wrap; - gap: var(--spacing-2xs); -} - -ul, -ol, -dl { - margin: var(--spacing-sm) 0; - - & & { - margin: var(--spacing-2xs) 0 0; - } - - ::marker { - color: var(--color-primary-dark); - } -} - a { background: linear-gradient(to top, var(--color-primary) 50%, transparent 50%) 0 0 / 100% 201% no-repeat; diff --git a/src/styles/pages/Page.module.scss b/src/styles/pages/Page.module.scss index 24fc1eb..5c2848e 100644 --- a/src/styles/pages/Page.module.scss +++ b/src/styles/pages/Page.module.scss @@ -25,10 +25,6 @@ text-align: center; } -.list { - @extend %reset-ordered-list; -} - li.item { margin: 0 0 var(--spacing-md) 0; border-bottom: fun.convert-px(1) solid var(--color-border); diff --git a/src/styles/pages/partials/_article-lists.scss b/src/styles/pages/partials/_article-lists.scss index c0084b0..e872d3c 100644 --- a/src/styles/pages/partials/_article-lists.scss +++ b/src/styles/pages/partials/_article-lists.scss @@ -1,39 +1,15 @@ -@mixin styles { - ol { - padding: 0; - list-style-type: none; - counter-reset: li; - - > li { - display: table; - counter-increment: li; +@use "../../../styles/abstracts/placeholders"; - &::before { - content: counters(li, ".") ". "; - display: table-cell; - padding-right: var(--spacing-2xs); - color: var(--color-secondary); - } - } - - li ol > li::before { - content: counters(li, ".") ". "; - } - } - - ul, - ol { - li:not(:last-child) { - margin-bottom: var(--spacing-2xs); - } +@mixin styles { + ol, + ul { + @extend %regular-list; - ::marker { - color: var(--color-primary-dark); - } + margin: var(--spacing-sm) 0; } - ul { - padding-left: var(--spacing-sm); + li { + @extend %list-item; } dl { @@ -41,11 +17,6 @@ flex-flow: row wrap; gap: var(--spacing-2xs); width: fit-content; - } - - ul, - ol, - dl { margin: var(--spacing-sm) 0; & & { diff --git a/src/styles/pages/partials/_article-wp-blocks.scss b/src/styles/pages/partials/_article-wp-blocks.scss index 6a847b1..f23be05 100644 --- a/src/styles/pages/partials/_article-wp-blocks.scss +++ b/src/styles/pages/partials/_article-wp-blocks.scss @@ -9,7 +9,8 @@ position: relative; border: fun.convert-px(1) solid var(--color-border-dark); border-left: fun.convert-px(5) solid var(--color-border-dark); - box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0 + box-shadow: + fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow), fun.convert-px(3) fun.convert-px(3) fun.convert-px(4) 0 var(--color-shadow); @@ -94,8 +95,6 @@ gap: var(--spacing-sm); .blocks-gallery-grid { - @extend %reset-list; - grid-column: 1 / -1; grid-row: 1 / -1; display: grid; @@ -110,7 +109,8 @@ a { display: block; - box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), + box-shadow: + 0 0 fun.convert-px(1) 0 var(--color-shadow), fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0 var(--color-shadow-light), fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0 @@ -119,7 +119,8 @@ &:hover, &:focus { transform: scale(1.05); - box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), + box-shadow: + 0 0 fun.convert-px(1) 0 var(--color-shadow), fun.convert-px(3) fun.convert-px(3) fun.convert-px(2) 0 var(--color-shadow-light), fun.convert-px(5) fun.convert-px(5) fun.convert-px(8) 0 @@ -132,7 +133,8 @@ &:active { transform: scale(0.95); - box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow), + box-shadow: + 0 0 fun.convert-px(1) 0 var(--color-shadow), fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0 var(--color-shadow-light), 0 0 0 0 var(--color-shadow-light); @@ -141,6 +143,7 @@ } } + // cSpell:ignore aligncenter &.aligncenter { .blocks-gallery-grid { align-items: center; diff --git a/src/types/app.ts b/src/types/app.ts index e237560..2e892b8 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -132,3 +132,6 @@ export type Slug = { }; export type Position = 'bottom' | 'center' | 'left' | 'right' | 'top'; + +/** Spacing keys defined has CSS variables */ +export type Spacing = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'; |
