From 15522ec9146f6f1956620355c44dea2a6a75b67c Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Mon, 9 Oct 2023 18:26:23 +0200 Subject: refactor(components): replace ResponsiveImage with Figure component The styles applied to ResponsiveImage are related to the figure and figcaption elements. Those elements could be use with other contents than images. So I extracted them in a Figure component. The ResponsiveImage component is no longer useful: the consumer should use the Image component from `next` and wrap it in a link if needed. --- src/components/atoms/figure/figure.module.scss | 30 +++++++++++ src/components/atoms/figure/figure.stories.tsx | 74 ++++++++++++++++++++++++++ src/components/atoms/figure/figure.test.tsx | 32 +++++++++++ src/components/atoms/figure/figure.tsx | 72 +++++++++++++++++++++++++ src/components/atoms/figure/index.ts | 1 + src/components/atoms/index.ts | 1 + 6 files changed, 210 insertions(+) create mode 100644 src/components/atoms/figure/figure.module.scss create mode 100644 src/components/atoms/figure/figure.stories.tsx create mode 100644 src/components/atoms/figure/figure.test.tsx create mode 100644 src/components/atoms/figure/figure.tsx create mode 100644 src/components/atoms/figure/index.ts (limited to 'src/components/atoms') diff --git a/src/components/atoms/figure/figure.module.scss b/src/components/atoms/figure/figure.module.scss new file mode 100644 index 0000000..e7ba5c2 --- /dev/null +++ b/src/components/atoms/figure/figure.module.scss @@ -0,0 +1,30 @@ +@use "../../../styles/abstracts/functions" as fun; + +.caption { + margin: 0; + padding: fun.convert-px(4) var(--spacing-2xs); + background: var(--color-bg-secondary); + border: fun.convert-px(1) solid var(--color-border-light); + font-size: var(--font-size-sm); + font-weight: 500; +} + +.wrapper { + display: flex; + flex-flow: column; + width: fit-content; + margin: 0 auto; + position: relative; + text-align: center; + + &--has-borders { + padding: fun.convert-px(4); + border: fun.convert-px(1) solid var(--color-border); + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0 + var(--color-shadow); + + .caption { + margin-top: fun.convert-px(4); + } + } +} diff --git a/src/components/atoms/figure/figure.stories.tsx b/src/components/atoms/figure/figure.stories.tsx new file mode 100644 index 0000000..7763641 --- /dev/null +++ b/src/components/atoms/figure/figure.stories.tsx @@ -0,0 +1,74 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import NextImage from 'next/image'; +import { Figure } from './figure'; + +/** + * Figure - Storybook Meta + */ +export default { + title: 'Atoms/Figure', + component: Figure, + args: {}, + argTypes: { + caption: { + control: { + type: 'text', + }, + description: 'A figure caption.', + table: { + category: 'Options', + }, + type: { + name: 'string', + required: false, + }, + }, + hasBorders: { + control: { + type: 'boolean', + }, + description: 'Add borders around the figure.', + table: { + category: 'Styles', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) =>
; + +/** + * Figure Stories - Illustration + */ +export const Illustration = Template.bind({}); +Illustration.args = { + children: ( + + ), +}; + +/** + * Figure Stories - BorderedIllustration + */ +export const BorderedIllustration = Template.bind({}); +BorderedIllustration.args = { + children: ( + + ), + hasBorders: true, +}; diff --git a/src/components/atoms/figure/figure.test.tsx b/src/components/atoms/figure/figure.test.tsx new file mode 100644 index 0000000..90a07c7 --- /dev/null +++ b/src/components/atoms/figure/figure.test.tsx @@ -0,0 +1,32 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Figure } from './figure'; + +describe('Figure', () => { + it('renders the figure contents', () => { + const body = 'tempora et quis'; + + render(
{body}
); + + expect(rtlScreen.getByRole('figure')).toHaveTextContent(body); + }); + + it('can render its contents with a caption', () => { + const body = 'tempora et quis'; + const caption = 'velit dolores magnam'; + + render(
{body}
); + + expect(rtlScreen.getByRole('figure', { name: caption })).toHaveTextContent( + body + ); + }); + + it('can style the figure with borders', () => { + const body = 'tempora et quis'; + + render(
{body}
); + + expect(rtlScreen.getByRole('figure')).toHaveClass('wrapper--has-borders'); + }); +}); diff --git a/src/components/atoms/figure/figure.tsx b/src/components/atoms/figure/figure.tsx new file mode 100644 index 0000000..4dd5b10 --- /dev/null +++ b/src/components/atoms/figure/figure.tsx @@ -0,0 +1,72 @@ +import { + forwardRef, + type ReactNode, + type ForwardRefRenderFunction, + type HTMLAttributes, + useId, +} from 'react'; +import styles from './figure.module.scss'; + +export type FigureProps = Omit, 'children'> & { + /** + * The contents (ie. an image, illustration, diagram, code snippet, etc.). + */ + children: ReactNode; + /** + * A figure caption. + */ + caption?: ReactNode; + /** + * Should we wrap the contents with borders? + */ + hasBorders?: boolean; +}; + +const FigureWithRef: ForwardRefRenderFunction = ( + { + 'aria-labelledby': ariaLabelledBy, + caption, + children, + className = '', + hasBorders, + ...props + }, + ref +) => { + const captionId = useId(); + const bordersModifier = hasBorders ? styles['wrapper--has-borders'] : ''; + const figureClass = `${styles.wrapper} ${bordersModifier} ${className}`; + + /** + * We need to ensure that the figcaption is used as an accessible name for the + * figure. In Testing Library, it is not automatically associated, it could + * also be the case in some browsers. However if the consumer provide its own + * `aria-labelled-by` attribute, it should be used instead of the caption (we + * could combine them but we cannot know which order is the more logical). + */ + const figureLabelledBy = + caption && !ariaLabelledBy ? captionId : ariaLabelledBy; + + return ( +
+ {children} + {caption ? ( +
+ {caption} +
+ ) : null} +
+ ); +}; + +/** + * Figure component + * + * Render a responsive image wrapped in a figure element. + */ +export const Figure = forwardRef(FigureWithRef); diff --git a/src/components/atoms/figure/index.ts b/src/components/atoms/figure/index.ts new file mode 100644 index 0000000..0f6ad20 --- /dev/null +++ b/src/components/atoms/figure/index.ts @@ -0,0 +1 @@ +export * from './figure'; diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts index e9c41ed..31beda9 100644 --- a/src/components/atoms/index.ts +++ b/src/components/atoms/index.ts @@ -1,4 +1,5 @@ export * from './buttons'; +export * from './figure'; export * from './flip'; export * from './forms'; export * from './heading'; -- cgit v1.2.3