From 36890cfafeba6e30782df1260d7f9e678c7da4bf Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Mon, 2 Oct 2023 17:01:57 +0200 Subject: refactor(components): rewrite DescriptionList component * add a `spacing` prop * replace `layout` prop with `isInline` prop * remove `items` prop (and classNames props) in favor of new components: Description, Group, Term * remove `withSeparator` prop (CSS content is announced by screen readers and Firefox/Safari have no support for alternative text so the consumer should add itself an element with `aria-hidden` if it need a separator) Be aware, Meta component and its consumers can be visually broken, they should be refactored before using them in production. --- .../description-list/description-list.module.scss | 28 ++++ .../description-list/description-list.stories.tsx | 150 +++++++++++++++++++++ .../description-list/description-list.test.tsx | 70 ++++++++++ .../lists/description-list/description-list.tsx | 67 +++++++++ .../atoms/lists/description-list/description.tsx | 28 ++++ .../atoms/lists/description-list/group.tsx | 62 +++++++++ .../atoms/lists/description-list/index.ts | 4 + .../atoms/lists/description-list/term.tsx | 28 ++++ 8 files changed, 437 insertions(+) create mode 100644 src/components/atoms/lists/description-list/description-list.module.scss create mode 100644 src/components/atoms/lists/description-list/description-list.stories.tsx create mode 100644 src/components/atoms/lists/description-list/description-list.test.tsx create mode 100644 src/components/atoms/lists/description-list/description-list.tsx create mode 100644 src/components/atoms/lists/description-list/description.tsx create mode 100644 src/components/atoms/lists/description-list/group.tsx create mode 100644 src/components/atoms/lists/description-list/index.ts create mode 100644 src/components/atoms/lists/description-list/term.tsx (limited to 'src/components/atoms/lists/description-list') diff --git a/src/components/atoms/lists/description-list/description-list.module.scss b/src/components/atoms/lists/description-list/description-list.module.scss new file mode 100644 index 0000000..951e1ee --- /dev/null +++ b/src/components/atoms/lists/description-list/description-list.module.scss @@ -0,0 +1,28 @@ +@use "../../../../styles/abstracts/placeholders"; + +.term { + @extend %term; +} + +.description { + @extend %description; +} + +.group { + width: fit-content; +} + +.list { + margin: 0; +} + +.group, +.list { + &--inline { + @extend %inline-description-list; + } + + &--stack { + @extend %stack-description-list; + } +} diff --git a/src/components/atoms/lists/description-list/description-list.stories.tsx b/src/components/atoms/lists/description-list/description-list.stories.tsx new file mode 100644 index 0000000..d051fcd --- /dev/null +++ b/src/components/atoms/lists/description-list/description-list.stories.tsx @@ -0,0 +1,150 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Description } from './description'; +import { DescriptionList } from './description-list'; +import { Group } from './group'; +import { Term } from './term'; + +/** + * DescriptionList - Storybook Meta + */ +export default { + title: 'Atoms/Lists/DescriptionList', + component: DescriptionList, + args: { + isInline: false, + }, + argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the list wrapper', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +/** + * Description List Stories - Single term, single description + */ +export const SingleTermSingleDescription = Template.bind({}); +SingleTermSingleDescription.args = { + children: ( + <> + A term + A description of the term. + + ), +}; + +/** + * Description List Stories - Multiple terms, single description + */ +export const MultipleTermsSingleDescription = Template.bind({}); +MultipleTermsSingleDescription.args = { + children: ( + <> + A first term + A second term + A third term + A description of the term. + + ), +}; + +/** + * Description List Stories - Single term, multiple descriptions + */ +export const SingleTermMultipleDescriptions = Template.bind({}); +SingleTermMultipleDescriptions.args = { + children: ( + <> + A term + A first description of the term. + A second description of the term. + A third description of the term. + + ), +}; + +/** + * Description List Stories - Multiple terms, multiple descriptions + */ +export const MultipleTermsMultipleDescriptions = Template.bind({}); +MultipleTermsMultipleDescriptions.args = { + children: ( + <> + A first term + A second term + A third term + A first description of the term. + A second description of the term. + A third description of the term. + + ), +}; + +/** + * Description List Stories - Group of terms & descriptions + */ +export const GroupOfTermsDescriptions = Template.bind({}); +GroupOfTermsDescriptions.args = { + children: ( + <> + + A term + A description of the term. + + + Another term + A description of the other term. + + + ), +}; + +/** + * Description List Stories - Inlined list of term and descriptions + */ +export const InlinedList = Template.bind({}); +InlinedList.args = { + children: ( + <> + A term: + A first description of the term. + A second description of the term. + A third description of the term. + + ), + isInline: true, + spacing: 'xs', +}; + +/** + * Description List Stories - Inlined group of terms & descriptions + */ +export const InlinedGroupOfTermsDescriptions = Template.bind({}); +InlinedGroupOfTermsDescriptions.args = { + children: ( + <> + + A term: + A description of the term. + + + Another term: + A description of the other term. + + + ), +}; diff --git a/src/components/atoms/lists/description-list/description-list.test.tsx b/src/components/atoms/lists/description-list/description-list.test.tsx new file mode 100644 index 0000000..3f9a1b5 --- /dev/null +++ b/src/components/atoms/lists/description-list/description-list.test.tsx @@ -0,0 +1,70 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Description } from './description'; +import { DescriptionList } from './description-list'; +import { Group } from './group'; +import { Term } from './term'; + +describe('DescriptionList', () => { + it('renders a list of terms and description', () => { + const term = 'A term'; + const desc = 'A description of the term.'; + + render( + + {term} + {desc} + + ); + + expect(rtlScreen.getByRole('definition')).toHaveTextContent(desc); + expect(rtlScreen.getByRole('term')).toHaveTextContent(term); + }); + + it('can renders a list of terms and description wrapped in a div', () => { + const term = 'A term'; + const desc = 'A description of the term.'; + + render( + + + {term} + {desc} + + + ); + + expect(rtlScreen.getByRole('definition')).toHaveTextContent(desc); + expect(rtlScreen.getByRole('term')).toHaveTextContent(term); + }); + + it('can render terms and description inlined', () => { + const term = 'A term'; + const desc = 'A description of the term.'; + + render( + + {term} + {desc} + + ); + + const list = rtlScreen.getByRole('term').parentElement; + expect(list).toHaveClass('list--inline'); + }); + + it('can render terms and description stacked', () => { + const term = 'A term'; + const desc = 'A description of the term.'; + + render( + + {term} + {desc} + + ); + + const list = rtlScreen.getByRole('term').parentElement; + expect(list).toHaveClass('list--stack'); + }); +}); diff --git a/src/components/atoms/lists/description-list/description-list.tsx b/src/components/atoms/lists/description-list/description-list.tsx new file mode 100644 index 0000000..cc225fe --- /dev/null +++ b/src/components/atoms/lists/description-list/description-list.tsx @@ -0,0 +1,67 @@ +import { + forwardRef, + type CSSProperties, + type HTMLAttributes, + type ReactNode, + type ForwardRefRenderFunction, +} from 'react'; +import type { Spacing } from '../../../../types'; +import styles from './description-list.module.scss'; + +export type DescriptionListProps = Omit< + HTMLAttributes, + 'children' +> & { + /** + * The list items or groups. + */ + children: ReactNode; + /** + * Should the list be inlined? + * + * @default false + */ + isInline?: boolean; + /** + * Define the spacing between list items. + * + * @default null + */ + spacing?: Spacing | null; +}; + +const DescriptionListWithRef: ForwardRefRenderFunction< + HTMLDListElement, + DescriptionListProps +> = ( + { + children, + className = '', + isInline = false, + spacing = null, + style, + ...props + }, + ref +) => { + const itemSpacing = spacing === null ? 0 : `var(--spacing-${spacing})`; + const layoutClass = styles[isInline ? 'list--inline' : 'list--stack']; + const listClass = `${styles.list} ${layoutClass} ${className}`; + const listStyles = { + ...style, + '--itemSpacing': itemSpacing, + } as CSSProperties; + + return ( +
+ {children} +
+ ); +}; + +/** + * DescriptionList component + * + * Render a description list. + */ +export const DescriptionList = forwardRef(DescriptionListWithRef); diff --git a/src/components/atoms/lists/description-list/description.tsx b/src/components/atoms/lists/description-list/description.tsx new file mode 100644 index 0000000..9fa7ecd --- /dev/null +++ b/src/components/atoms/lists/description-list/description.tsx @@ -0,0 +1,28 @@ +import { + forwardRef, + type ForwardRefRenderFunction, + type HTMLAttributes, +} from 'react'; +import styles from './description-list.module.scss'; + +export type DescriptionProps = HTMLAttributes; + +const DescriptionWithRef: ForwardRefRenderFunction< + HTMLElement, + DescriptionProps +> = ({ children, className = '', ...props }, ref) => { + const descriptionClass = `${styles.description} ${className}`; + + return ( +
+ {children} +
+ ); +}; + +/** + * Description component. + * + * Use it inside a `DescriptionList` or a `Group` component. + */ +export const Description = forwardRef(DescriptionWithRef); diff --git a/src/components/atoms/lists/description-list/group.tsx b/src/components/atoms/lists/description-list/group.tsx new file mode 100644 index 0000000..2d1fb4b --- /dev/null +++ b/src/components/atoms/lists/description-list/group.tsx @@ -0,0 +1,62 @@ +import { + forwardRef, + type CSSProperties, + type HTMLAttributes, + type ReactNode, + type ForwardRefRenderFunction, +} from 'react'; +import type { Spacing } from '../../../../types'; +import styles from './description-list.module.scss'; + +export type GroupProps = Omit, 'children'> & { + /** + * The term(s) and description(s) of a description list. + */ + children: ReactNode; + /** + * Should the term & description in the group be inlined? + * + * @default false + */ + isInline?: boolean; + /** + * Define the spacing between list items. + * + * @default null + */ + spacing?: Spacing | null; +}; + +const GroupWithRef: ForwardRefRenderFunction = ( + { + children, + className = '', + isInline = false, + spacing = null, + style, + ...props + }, + ref +) => { + const itemSpacing = spacing === null ? 0 : `var(--spacing-${spacing})`; + const layoutClass = styles[isInline ? 'group--inline' : 'group--stack']; + const groupClass = `${styles.group} ${layoutClass} ${className}`; + const groupStyles = { + ...style, + '--itemSpacing': itemSpacing, + } as CSSProperties; + + return ( +
+ {children} +
+ ); +}; + +/** + * Group component. + * + * Use it to wrap `Description` and `Term` components in a `DescriptionList` + * component. + */ +export const Group = forwardRef(GroupWithRef); diff --git a/src/components/atoms/lists/description-list/index.ts b/src/components/atoms/lists/description-list/index.ts new file mode 100644 index 0000000..7f67579 --- /dev/null +++ b/src/components/atoms/lists/description-list/index.ts @@ -0,0 +1,4 @@ +export * from './description'; +export * from './description-list'; +export * from './group'; +export * from './term'; diff --git a/src/components/atoms/lists/description-list/term.tsx b/src/components/atoms/lists/description-list/term.tsx new file mode 100644 index 0000000..0d21f96 --- /dev/null +++ b/src/components/atoms/lists/description-list/term.tsx @@ -0,0 +1,28 @@ +import { + forwardRef, + type ForwardRefRenderFunction, + type HTMLAttributes, +} from 'react'; +import styles from './description-list.module.scss'; + +export type TermProps = HTMLAttributes; + +const TermWithRef: ForwardRefRenderFunction = ( + { children, className = '', ...props }, + ref +) => { + const termClass = `${styles.term} ${className}`; + + return ( +
+ {children} +
+ ); +}; + +/** + * Term component. + * + * Use it inside a `DescriptionList` or a `Group` component. + */ +export const Term = forwardRef(TermWithRef); -- cgit v1.2.3