diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-06 17:48:03 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:14:41 +0100 |
| commit | 12a03a9a72f7895d571dbaeeb245d92aa277a610 (patch) | |
| tree | 41b6b07928e4f5e101b7ea5d8389bb4325bbac76 /src/components | |
| parent | fb860884857da73ee5b5e897745301cdf1d770a2 (diff) | |
refactor(components): merge HeadingButton and Widget components
The HeadingButton component was only used inside Widget component and
it is not very useful on its own so I merge the two components in a
new Collapsible component.
Diffstat (limited to 'src/components')
31 files changed, 515 insertions, 722 deletions
diff --git a/src/components/molecules/buttons/heading-button.module.scss b/src/components/molecules/buttons/heading-button.module.scss deleted file mode 100644 index 689f1e6..0000000 --- a/src/components/molecules/buttons/heading-button.module.scss +++ /dev/null @@ -1,44 +0,0 @@ -@use "../../../styles/abstracts/functions" as fun; - -.icon { - transition: all 0.25s ease-in-out 0s; -} - -.wrapper { - display: flex; - flex-flow: row nowrap; - align-items: center; - justify-content: space-between; - gap: var(--spacing-md); - width: 100%; - padding: 0 var(--spacing-2xs); - position: sticky; - top: 0; - background: inherit; - border: none; - border-top: fun.convert-px(2) solid var(--color-primary-dark); - border-bottom: fun.convert-px(2) solid var(--color-primary-dark); - cursor: pointer; - - .heading { - padding: var(--spacing-2xs) 0; - background: none; - font-size: var(--font-size-xl); - font-weight: 500; - text-align: left; - } - - &:hover, - &:focus { - .icon { - background: var(--color-primary-light); - color: var(--color-fg-inverted); - transform: scale(1.25); - - &::before, - &::after { - background: var(--color-bg); - } - } - } -} diff --git a/src/components/molecules/buttons/heading-button.stories.tsx b/src/components/molecules/buttons/heading-button.stories.tsx deleted file mode 100644 index 9beda2b..0000000 --- a/src/components/molecules/buttons/heading-button.stories.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { useState } from 'react'; -import { HeadingButton as HeadingButtonComponent } from './heading-button'; - -/** - * HeadingButton - Storybook Meta - */ -export default { - title: 'Molecules/Buttons/HeadingButton', - component: HeadingButtonComponent, - argTypes: { - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the button.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - expanded: { - control: { - type: null, - }, - description: 'Heading button state (plus or minus).', - type: { - name: 'boolean', - required: true, - }, - }, - level: { - control: { - type: 'number', - min: 1, - max: 6, - }, - description: 'Heading level.', - type: { - name: 'number', - required: true, - }, - }, - setExpanded: { - control: { - type: null, - }, - description: 'Callback function to set heading button state.', - type: { - name: 'function', - required: true, - }, - }, - title: { - control: { - type: 'text', - }, - description: 'Heading title.', - type: { - name: 'string', - required: true, - }, - }, - }, -} as ComponentMeta<typeof HeadingButtonComponent>; - -const Template: ComponentStory<typeof HeadingButtonComponent> = ({ - expanded, - setExpanded: _setExpanded, - ...args -}) => { - const [isExpanded, setIsExpanded] = useState<boolean>(expanded); - - return ( - <HeadingButtonComponent - expanded={isExpanded} - setExpanded={setIsExpanded} - {...args} - /> - ); -}; - -/** - * Heading Button Stories - Expanded - */ -export const Expanded = Template.bind({}); -Expanded.args = { - expanded: true, - level: 2, - title: 'Your title', -}; - -/** - * Heading Button Stories - Collapsed - */ -export const Collapsed = Template.bind({}); -Collapsed.args = { - expanded: false, - level: 2, - title: 'Your title', -}; diff --git a/src/components/molecules/buttons/heading-button.test.tsx b/src/components/molecules/buttons/heading-button.test.tsx deleted file mode 100644 index 4d3d91e..0000000 --- a/src/components/molecules/buttons/heading-button.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { HeadingButton } from './heading-button'; - -describe('HeadingButton', () => { - it('renders a button to collapse.', () => { - render( - <HeadingButton - level={2} - title="The accordion title" - expanded={true} - setExpanded={() => null} - /> - ); - expect( - screen.getByRole('button', { name: 'Collapse The accordion title' }) - ).toBeInTheDocument(); - }); - - it('renders a button to expand.', () => { - render( - <HeadingButton - level={2} - title="The accordion title" - expanded={false} - setExpanded={() => null} - /> - ); - expect( - screen.getByRole('button', { name: 'Expand The accordion title' }) - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/molecules/buttons/heading-button.tsx b/src/components/molecules/buttons/heading-button.tsx deleted file mode 100644 index 3c3eef5..0000000 --- a/src/components/molecules/buttons/heading-button.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useCallback, type FC, type SetStateAction } from 'react'; -import { useIntl } from 'react-intl'; -import { Heading, type HeadingProps, Icon } from '../../atoms'; -import styles from './heading-button.module.scss'; - -export type HeadingButtonProps = Pick<HeadingProps, 'level'> & { - /** - * Set additional classnames to the button. - */ - className?: string; - /** - * Accordion state. - */ - expanded: boolean; - /** - * Callback function to set accordion state on click. - */ - setExpanded: (value: SetStateAction<boolean>) => void; - /** - * Accordion title. - */ - title: string; -}; - -/** - * HeadingButton component - * - * Render a button as accordion title to toggle body. - */ -export const HeadingButton: FC<HeadingButtonProps> = ({ - className = '', - expanded, - level, - setExpanded, - title, -}) => { - const intl = useIntl(); - const btnClass = `${styles.wrapper} ${className}`; - const iconState = expanded ? 'minus' : 'plus'; - const titlePrefix = expanded - ? intl.formatMessage({ - defaultMessage: 'Collapse', - description: 'HeadingButton: title prefix (expanded state)', - id: 'UX9Bu8', - }) - : intl.formatMessage({ - defaultMessage: 'Expand', - description: 'HeadingButton: title prefix (collapsed state)', - id: 'bcyOgC', - }); - - const toggleExpand = useCallback( - () => setExpanded((prev) => !prev), - [setExpanded] - ); - - return ( - <button className={btnClass} onClick={toggleExpand} type="button"> - <Heading className={styles.heading} level={level}> - {/* eslint-disable-next-line react/jsx-no-literals -- SR class allowed */} - <span className="screen-reader-text">{titlePrefix} </span> - {title} - </Heading> - <Icon className={styles.icon} shape={iconState} /> - </button> - ); -}; diff --git a/src/components/molecules/buttons/index.ts b/src/components/molecules/buttons/index.ts index e0a10c1..b2930aa 100644 --- a/src/components/molecules/buttons/index.ts +++ b/src/components/molecules/buttons/index.ts @@ -1,3 +1,2 @@ export * from './back-to-top'; -export * from './heading-button'; export * from './help-button'; diff --git a/src/components/molecules/collapsible/collapsible.module.scss b/src/components/molecules/collapsible/collapsible.module.scss new file mode 100644 index 0000000..3c5a97c --- /dev/null +++ b/src/components/molecules/collapsible/collapsible.module.scss @@ -0,0 +1,100 @@ +@use "../../../styles/abstracts/functions" as fun; +@use "../../../styles/abstracts/mixins" as mix; + +.heading { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + gap: var(--spacing-md); + width: 100%; + padding: var(--spacing-2xs); + position: sticky; + top: 0; + z-index: 2; + background: var(--color-bg); + border-top: fun.convert-px(2) solid var(--color-primary-dark); + border-bottom: fun.convert-px(2) solid var(--color-primary-dark); + + &:hover, + &:focus { + .icon { + background: var(--color-primary-light); + color: var(--color-fg-inverted); + transform: scale(1.25); + + &::before, + &::after { + background: var(--color-bg); + } + } + } +} + +.body { + &--has-borders { + border: fun.convert-px(2) solid var(--color-primary-dark); + } + + &--has-padding { + padding-inline: var(--spacing-2xs); + } +} + +.wrapper { + display: flex; + flex-flow: column; + + @include mix.media("screen") { + @include mix.dimensions("lg") { + max-height: calc(100vh - var(--spacing-2xs)); + + &--expanded { + .body { + overflow: hidden; + } + + &:hover, + &:focus-within { + .body { + overflow-y: auto; + } + } + } + } + } + + &--collapsed { + .body { + max-height: 0; + opacity: 0; + overflow: hidden; + visibility: hidden; + transition: + all 0.2s linear 0.2s, + max-height 0.5s cubic-bezier(0, 1, 0, 1); + } + } + + &--expanded { + .body { + max-height: 10000px; // Fixed value needed for transition. + opacity: 1; + visibility: visible; + transition: + all 0.5s ease-in-out 0s, + margin 0.2s ease-in-out 0s, + padding 0.2s ease-in-out 0s, + max-height 1.2s ease-in-out; + + &--has-padding { + margin: var(--spacing-2xs) 0; + padding-block: var(--spacing-2xs); + } + + &--no-padding { + margin: var(--spacing-xs) 0; + } + } + } +} diff --git a/src/components/molecules/collapsible/collapsible.stories.tsx b/src/components/molecules/collapsible/collapsible.stories.tsx new file mode 100644 index 0000000..7cac64d --- /dev/null +++ b/src/components/molecules/collapsible/collapsible.stories.tsx @@ -0,0 +1,72 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Heading } from '../../atoms'; +import { Collapsible } from './collapsible'; + +/** + * HeadingButton - Storybook Meta + */ +export default { + title: 'Molecules/Collapsible', + component: Collapsible, + argTypes: { + heading: { + control: { + type: 'text', + }, + description: 'Define the collapsible heading.', + type: { + name: 'function', + required: true, + }, + }, + isCollapsed: { + control: { + type: 'boolean', + }, + description: 'Define if the component should be collapsed or expanded.', + table: { + category: 'Options', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: true, + }, + }, + }, +} as ComponentMeta<typeof Collapsible>; + +const Template: ComponentStory<typeof Collapsible> = ({ heading, ...args }) => ( + <Collapsible + {...args} + heading={ + <Heading isFake level={3}> + {heading} + </Heading> + } + /> +); + +const heading = 'Your title'; +const body = + 'Eius et eum ex voluptas laboriosam aliquid quas necessitatibus. Molestiae eius voluptatem qui voluptas eaque et totam. Ut ipsum ea sit. Quos molestiae id est consequatur. Suscipit illo at. Omnis non suscipit. Qui itaque laboriosam quos ut est laudantium. Iusto recusandae excepturi quia labore voluptatem quod recusandae. Quod ducimus ut rem dolore et.'; + +/** + * Collapsible Stories - Collapsed + */ +export const Collapsed = Template.bind({}); +Collapsed.args = { + children: body, + heading, + isCollapsed: true, +}; + +/** + * Collapsible Stories - Expanded + */ +export const Expanded = Template.bind({}); +Expanded.args = { + children: body, + heading, + isCollapsed: false, +}; diff --git a/src/components/molecules/collapsible/collapsible.test.tsx b/src/components/molecules/collapsible/collapsible.test.tsx new file mode 100644 index 0000000..52fbdd0 --- /dev/null +++ b/src/components/molecules/collapsible/collapsible.test.tsx @@ -0,0 +1,83 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { Collapsible } from './collapsible'; +import { Heading } from 'src/components/atoms'; + +const body = + 'Error autem numquam vero quo cum qui voluptatem est qui. Quasi id rem molestiae. Velit voluptatum dolores et officia. Ut voluptatem quae ut quaerat vel nulla.'; +const heading = 'sed minima sed'; + +describe('Collapsible', () => { + it('renders the collapsible heading and body', () => { + const headingLevel = 2; + + render( + <Collapsible heading={<Heading level={headingLevel}>{heading}</Heading>}> + {body} + </Collapsible> + ); + + expect( + rtlScreen.getByRole('heading', { level: headingLevel }) + ).toHaveTextContent(heading); + expect(rtlScreen.getByText(body)).toBeInTheDocument(); + }); + + it('can be collapsed by default', () => { + render( + <Collapsible heading={<Heading level={3}>{heading}</Heading>} isCollapsed> + {body} + </Collapsible> + ); + + expect(rtlScreen.getByRole('button').parentElement).toHaveClass( + 'wrapper--collapsed' + ); + // Neither toBeVisible or toHaveStyle are working. + // expect(rtlScreen.getByText(body)).toHaveStyle({ visibility: 'hidden' }); + // expect(rtlScreen.getByText(body)).not.toBeVisible(); + }); + + it('can be expanded by default', () => { + render( + <Collapsible + heading={<Heading level={3}>{heading}</Heading>} + isCollapsed={false} + > + {body} + </Collapsible> + ); + + expect(rtlScreen.getByRole('button').parentElement).toHaveClass( + 'wrapper--expanded' + ); + expect(rtlScreen.getByText(body)).toBeVisible(); + }); + + it('can be collapsed and/or expanded by the user', async () => { + const user = userEvent.setup(); + + render( + <Collapsible heading={<Heading level={3}>{heading}</Heading>}> + {body} + </Collapsible> + ); + + expect(rtlScreen.getByRole('button').parentElement).toHaveClass( + 'wrapper--expanded' + ); + + await user.click(rtlScreen.getByRole('button', { name: heading })); + + expect(rtlScreen.getByRole('button').parentElement).toHaveClass( + 'wrapper--collapsed' + ); + + await user.click(rtlScreen.getByRole('button', { name: heading })); + + expect(rtlScreen.getByRole('button').parentElement).toHaveClass( + 'wrapper--expanded' + ); + }); +}); diff --git a/src/components/molecules/collapsible/collapsible.tsx b/src/components/molecules/collapsible/collapsible.tsx new file mode 100644 index 0000000..e61ccba --- /dev/null +++ b/src/components/molecules/collapsible/collapsible.tsx @@ -0,0 +1,108 @@ +import { + useCallback, + type ForwardRefRenderFunction, + type HTMLAttributes, + type ReactNode, + forwardRef, + useId, + useState, +} from 'react'; +import { Button, Icon } from '../../atoms'; +import styles from './collapsible.module.scss'; + +export type CollapsibleProps = Omit< + HTMLAttributes<HTMLDivElement>, + 'children' +> & { + /** + * The collapsible body. + */ + children: ReactNode; + /** + * Should we disable padding around body? + * + * @default false + */ + disablePadding?: boolean; + /** + * Should the body be bordered? + * + * @default false + */ + hasBorders?: boolean; + /** + * The collapsible heading. + */ + heading: ReactNode; + /** + * Should the component be collapsed or expanded by default? + * + * @default false + */ + isCollapsed?: boolean; +}; + +const CollapsibleWithRef: ForwardRefRenderFunction< + HTMLDivElement, + CollapsibleProps +> = ( + { + children, + className = '', + disablePadding = false, + hasBorders = false, + heading, + isCollapsed = false, + ...props + }, + ref +) => { + const bodyId = useId(); + const [isExpanded, setIsExpanded] = useState(!isCollapsed); + const bodyClassNames = [ + styles.body, + hasBorders ? styles['body--has-borders'] : '', + styles[disablePadding ? 'body--no-padding' : 'body--has-padding'], + ]; + const wrapperClassNames = [ + styles.wrapper, + styles[isExpanded ? 'wrapper--expanded' : 'wrapper--collapsed'], + className, + ]; + + const handleState = useCallback(() => { + setIsExpanded((prevState) => !prevState); + }, []); + + return ( + <div {...props} className={wrapperClassNames.join(' ')} ref={ref}> + <Button + aria-controls={bodyId} + aria-expanded={isExpanded} + className={styles.heading} + // eslint-disable-next-line react/jsx-no-literals -- Kind allowed + kind="neutral" + onClick={handleState} + // eslint-disable-next-line react/jsx-no-literals -- Shape allowed + shape="initial" + > + {heading} + <Icon + aria-hidden + className={styles.icon} + shape={isExpanded ? 'minus' : 'plus'} + /> + </Button> + <div className={bodyClassNames.join(' ')} id={bodyId}> + {children} + </div> + </div> + ); +}; + +/** + * Collapsible component. + * + * Render a heading associated to a collapsible body. + */ +export const Collapsible = forwardRef(CollapsibleWithRef); diff --git a/src/components/molecules/collapsible/index.ts b/src/components/molecules/collapsible/index.ts new file mode 100644 index 0000000..20c0813 --- /dev/null +++ b/src/components/molecules/collapsible/index.ts @@ -0,0 +1 @@ +export * from './collapsible'; diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts index dae369b..a62f3bf 100644 --- a/src/components/molecules/index.ts +++ b/src/components/molecules/index.ts @@ -1,4 +1,5 @@ export * from './buttons'; +export * from './collapsible'; export * from './forms'; export * from './images'; export * from './layout'; diff --git a/src/components/molecules/layout/index.ts b/src/components/molecules/layout/index.ts index 9fa1216..1580baa 100644 --- a/src/components/molecules/layout/index.ts +++ b/src/components/molecules/layout/index.ts @@ -5,4 +5,3 @@ export * from './columns'; export * from './meta'; export * from './page-footer'; export * from './page-header'; -export * from './widget'; diff --git a/src/components/molecules/layout/widget.module.scss b/src/components/molecules/layout/widget.module.scss deleted file mode 100644 index 1a601e5..0000000 --- a/src/components/molecules/layout/widget.module.scss +++ /dev/null @@ -1,65 +0,0 @@ -@use "../../../styles/abstracts/functions" as fun; -@use "../../../styles/abstracts/mixins" as mix; - -.widget { - display: flex; - flex-flow: column; - - &__header { - z-index: 2; - background: var(--color-bg); - } - - &__body { - position: relative; - } - - &--has-borders & { - &__body { - border: fun.convert-px(2) solid var(--color-primary-dark); - } - } - - &--collapsed & { - &__body { - max-height: 0; - margin: 0; - visibility: hidden; - opacity: 0; - overflow: hidden; - border: 0 solid transparent; - transition: all 0.1s linear 0.3s, - max-height 0.5s cubic-bezier(0, 1, 0, 1) 0s, margin 0.3s ease-in-out 0s; - } - } - - &--expanded#{&}--has-scroll { - @include mix.media("screen") { - @include mix.dimensions("lg") { - max-height: 95vh; - - .widget__body { - overflow: hidden; - } - - &:hover, - &:focus-within { - .widget__body { - overflow-y: auto; - } - } - } - } - } - - &--expanded & { - &__body { - max-height: 10000px; // needs a fixed value for transition. - margin: var(--spacing-sm) 0; - opacity: 1; - visibility: visible; - transition: all 0.5s ease-in-out 0s, border 0s linear 0s, - max-height 0.6s ease-in-out 0s; - } - } -} diff --git a/src/components/molecules/layout/widget.stories.tsx b/src/components/molecules/layout/widget.stories.tsx deleted file mode 100644 index 8fb868d..0000000 --- a/src/components/molecules/layout/widget.stories.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import headingButtonStories from '../buttons/heading-button.stories'; -import { Widget } from './widget'; - -/** - * Widget - Storybook Meta - */ -export default { - title: 'Molecules/Layout/Widget', - component: Widget, - args: { - withBorders: false, - withScroll: false, - }, - argTypes: { - children: { - control: { - type: 'text', - }, - description: 'The widget body', - type: { - name: 'string', - required: true, - }, - }, - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the widget wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - expanded: { - control: { - type: 'boolean', - }, - description: 'The widget state (expanded or collapsed)', - table: { - category: 'Options', - defaultValue: { summary: true }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - level: headingButtonStories.argTypes?.level, - title: { - control: { - type: 'text', - }, - description: 'The widget title.', - type: { - name: 'string', - required: true, - }, - }, - withBorders: { - control: { - type: 'boolean', - }, - description: 'Define if the content should have borders.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - withScroll: { - control: { - type: 'boolean', - }, - description: 'Define if the widget should be scrollable', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - }, -} as ComponentMeta<typeof Widget>; - -const Template: ComponentStory<typeof Widget> = (args) => <Widget {...args} />; - -/** - * Widget Stories - Expanded - */ -export const Expanded = Template.bind({}); -Expanded.args = { - children: 'Widget body', - expanded: true, - level: 2, - title: 'Widget title', -}; - -/** - * Widget Stories - Collapsed - */ -export const Collapsed = Template.bind({}); -Collapsed.args = { - children: 'Widget body', - expanded: false, - level: 2, - title: 'Widget title', -}; diff --git a/src/components/molecules/layout/widget.test.tsx b/src/components/molecules/layout/widget.test.tsx deleted file mode 100644 index 21c7a3c..0000000 --- a/src/components/molecules/layout/widget.test.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { Widget } from './widget'; - -const children = 'Widget body'; -const title = 'Widget title'; -const titleLevel = 2; - -describe('Widget', () => { - it('renders the widget title', () => { - render( - <Widget expanded={true} title={title} level={titleLevel}> - {children} - </Widget> - ); - expect( - screen.getByRole('heading', { level: titleLevel }) - ).toHaveTextContent(title); - }); -}); diff --git a/src/components/molecules/layout/widget.tsx b/src/components/molecules/layout/widget.tsx deleted file mode 100644 index 0bb04c7..0000000 --- a/src/components/molecules/layout/widget.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { FC, ReactNode, useState } from 'react'; -import { HeadingButton, type HeadingButtonProps } from '../buttons'; -import styles from './widget.module.scss'; - -export type WidgetProps = Pick< - HeadingButtonProps, - 'expanded' | 'level' | 'title' -> & { - /** - * The widget body. - */ - children: ReactNode; - /** - * Set additional classnames to the widget wrapper. - */ - className?: string; - /** - * Determine if the widget body should have borders. Default: false. - */ - withBorders?: boolean; - /** - * Determine if a vertical scrollbar should be displayed. Default: false. - */ - withScroll?: boolean; -}; - -/** - * Widget component - * - * Render an expandable widget. - */ -export const Widget: FC<WidgetProps> = ({ - children, - className = '', - expanded = true, - level, - title, - withBorders = false, - withScroll = false, -}) => { - const [isExpanded, setIsExpanded] = useState<boolean>(expanded); - const stateClass = isExpanded ? 'widget--expanded' : 'widget--collapsed'; - const bordersClass = withBorders - ? 'widget--has-borders' - : 'widget--no-borders'; - const scrollClass = withScroll ? 'widget--has-scroll' : 'widget--no-scroll'; - const widgetClass = `${styles.widget} ${styles[bordersClass]} ${styles[stateClass]} ${styles[scrollClass]} ${className}`; - - return ( - <div className={widgetClass}> - <HeadingButton - level={level} - title={title} - expanded={isExpanded} - setExpanded={setIsExpanded} - className={styles.widget__header} - /> - <div className={styles.widget__body}>{children}</div> - </div> - ); -}; diff --git a/src/components/organisms/widgets/image-widget.stories.tsx b/src/components/organisms/widgets/image-widget.stories.tsx index 9460060..e9857bf 100644 --- a/src/components/organisms/widgets/image-widget.stories.tsx +++ b/src/components/organisms/widgets/image-widget.stories.tsx @@ -1,4 +1,5 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Heading } from '../../atoms'; import { ImageWidget } from './image-widget'; /** @@ -52,16 +53,6 @@ export default { required: false, }, }, - expanded: { - control: { - type: 'boolean', - }, - description: 'The state of the widget.', - type: { - name: 'boolean', - required: true, - }, - }, image: { description: 'An image object.', type: { @@ -83,28 +74,6 @@ export default { required: false, }, }, - level: { - control: { - type: 'number', - min: 1, - max: 6, - }, - description: 'The widget title level (hn).', - type: { - name: 'number', - required: true, - }, - }, - title: { - control: { - type: 'text', - }, - description: 'The widget title.', - type: { - name: 'string', - required: true, - }, - }, url: { control: { type: 'text', @@ -128,7 +97,7 @@ const Template: ComponentStory<typeof ImageWidget> = (args) => ( const image = { alt: 'Et perferendis quaerat', height: 480, - src: 'http://placeimg.com/640/480/nature', + src: 'http://picsum.photos/640/480', width: 640, }; @@ -138,10 +107,12 @@ const image = { export const AlignLeft = Template.bind({}); AlignLeft.args = { alignment: 'left', - expanded: true, + heading: ( + <Heading isFake level={3}> + Quo et totam + </Heading> + ), image, - level: 2, - title: 'Quo et totam', }; /** @@ -150,10 +121,12 @@ AlignLeft.args = { export const AlignCenter = Template.bind({}); AlignCenter.args = { alignment: 'center', - expanded: true, + heading: ( + <Heading isFake level={3}> + Quo et totam + </Heading> + ), image, - level: 2, - title: 'Quo et totam', }; /** @@ -162,10 +135,12 @@ AlignCenter.args = { export const AlignRight = Template.bind({}); AlignRight.args = { alignment: 'right', - expanded: true, + heading: ( + <Heading isFake level={3}> + Quo et totam + </Heading> + ), image, - level: 2, - title: 'Quo et totam', }; /** @@ -174,8 +149,10 @@ AlignRight.args = { export const WithDescription = Template.bind({}); WithDescription.args = { description: 'Sint enim harum', - expanded: true, + heading: ( + <Heading isFake level={3}> + Quo et totam + </Heading> + ), image, - level: 2, - title: 'Quo et totam', }; diff --git a/src/components/organisms/widgets/image-widget.test.tsx b/src/components/organisms/widgets/image-widget.test.tsx index 7f3ccd8..3d48947 100644 --- a/src/components/organisms/widgets/image-widget.test.tsx +++ b/src/components/organisms/widgets/image-widget.test.tsx @@ -1,5 +1,6 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Heading } from '../../atoms'; import { ImageWidget } from './image-widget'; const description = 'Ut vitae sit'; @@ -20,26 +21,22 @@ describe('ImageWidget', () => { it('renders an image', () => { render( <ImageWidget - expanded={true} + heading={<Heading level={titleLevel}>{title}</Heading>} image={img} - title={title} - level={titleLevel} /> ); - expect(screen.getByRole('img', { name: img.alt })).toBeInTheDocument(); + expect(rtlScreen.getByRole('img', { name: img.alt })).toBeInTheDocument(); }); - it('renders a link', () => { + it('renders an image with a link', () => { render( <ImageWidget - expanded={true} + heading={<Heading level={titleLevel}>{title}</Heading>} image={img} - title={title} - level={titleLevel} url={url} /> ); - expect(screen.getByRole('link', { name: img.alt })).toHaveAttribute( + expect(rtlScreen.getByRole('link', { name: img.alt })).toHaveAttribute( 'href', url ); @@ -48,13 +45,11 @@ describe('ImageWidget', () => { it('renders a description', () => { render( <ImageWidget - expanded={true} + heading={<Heading level={titleLevel}>{title}</Heading>} image={img} description={description} - title={title} - level={titleLevel} /> ); - expect(screen.getByText(description)).toBeInTheDocument(); + expect(rtlScreen.getByText(description)).toBeInTheDocument(); }); }); diff --git a/src/components/organisms/widgets/image-widget.tsx b/src/components/organisms/widgets/image-widget.tsx index f3dc92f..07c4b11 100644 --- a/src/components/organisms/widgets/image-widget.tsx +++ b/src/components/organisms/widgets/image-widget.tsx @@ -1,9 +1,9 @@ -import { FC } from 'react'; +import type { FC } from 'react'; import { ResponsiveImage, type ResponsiveImageProps, - Widget, - type WidgetProps, + Collapsible, + type CollapsibleProps, } from '../../molecules'; import styles from './image-widget.module.scss'; @@ -14,9 +14,9 @@ export type Image = Pick< 'alt' | 'height' | 'src' | 'width' >; -export type ImageWidgetProps = Pick< - WidgetProps, - 'className' | 'expanded' | 'level' | 'title' +export type ImageWidgetProps = Omit< + CollapsibleProps, + 'children' | 'onToggle' > & { /** * The content alignment. @@ -51,19 +51,23 @@ export const ImageWidget: FC<ImageWidgetProps> = ({ description, image, imageClassName = '', + isCollapsed, url, ...props }) => { const alignmentClass = `widget--${alignment}`; return ( - <Widget className={`${styles[alignmentClass]} ${className}`} {...props}> + <Collapsible + {...props} + className={`${styles[alignmentClass]} ${className}`} + > <ResponsiveImage {...image} caption={description} className={`${styles.figure} ${imageClassName}`} target={url} /> - </Widget> + </Collapsible> ); }; diff --git a/src/components/organisms/widgets/links-list-widget.stories.tsx b/src/components/organisms/widgets/links-list-widget.stories.tsx index 2180de4..6e5f170 100644 --- a/src/components/organisms/widgets/links-list-widget.stories.tsx +++ b/src/components/organisms/widgets/links-list-widget.stories.tsx @@ -1,4 +1,5 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Heading } from '../../atoms'; import { LinksListWidget } from './links-list-widget'; /** @@ -32,28 +33,6 @@ export default { value: {}, }, }, - level: { - control: { - type: 'number', - min: 1, - max: 6, - }, - description: 'The heading level.', - type: { - name: 'number', - required: true, - }, - }, - title: { - control: { - type: 'text', - }, - description: 'The widget title.', - type: { - name: 'string', - required: true, - }, - }, }, } as ComponentMeta<typeof LinksListWidget>; @@ -89,9 +68,12 @@ const items = [ */ export const Unordered = Template.bind({}); Unordered.args = { + heading: ( + <Heading isFake level={3}> + Quo et totam + </Heading> + ), items, - level: 2, - title: 'A list of links', }; /** @@ -99,8 +81,11 @@ Unordered.args = { */ export const Ordered = Template.bind({}); Ordered.args = { + heading: ( + <Heading isFake level={3}> + Quo et totam + </Heading> + ), isOrdered: true, items, - 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 6323e19..2a914e7 100644 --- a/src/components/organisms/widgets/links-list-widget.test.tsx +++ b/src/components/organisms/widgets/links-list-widget.test.tsx @@ -1,5 +1,6 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../tests/utils'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Heading } from '../../atoms'; import { LinksListWidget } from './links-list-widget'; const title = 'Voluptatem minus autem'; @@ -12,19 +13,42 @@ const items = [ describe('LinksListWidget', () => { it('renders a widget title', () => { - render(<LinksListWidget items={items} title={title} level={2} />); + render( + <LinksListWidget + heading={<Heading level={3}>{title}</Heading>} + items={items} + /> + ); expect( - rtlScreen.getByRole('heading', { level: 2, name: new RegExp(title, 'i') }) + rtlScreen.getByRole('heading', { level: 3, name: new RegExp(title, 'i') }) ).toBeInTheDocument(); }); it('renders the correct number of items', () => { - render(<LinksListWidget items={items} title={title} level={2} />); + render( + <LinksListWidget + heading={ + <Heading isFake level={3}> + {title} + </Heading> + } + items={items} + /> + ); expect(rtlScreen.getAllByRole('listitem')).toHaveLength(items.length); }); it('renders some links', () => { - render(<LinksListWidget items={items} title={title} level={2} />); + render( + <LinksListWidget + heading={ + <Heading isFake level={3}> + {title} + </Heading> + } + items={items} + /> + ); 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 8f71efd..17a5884 100644 --- a/src/components/organisms/widgets/links-list-widget.tsx +++ b/src/components/organisms/widgets/links-list-widget.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react'; import { slugify } from '../../../utils/helpers'; import { Link, List, ListItem } from '../../atoms'; -import { Widget, type WidgetProps } from '../../molecules'; +import { Collapsible, type CollapsibleProps } from '../../molecules'; import styles from './links-list-widget.module.scss'; export type LinksListItems = { @@ -19,7 +19,10 @@ export type LinksListItems = { url: string; }; -export type LinksListWidgetProps = Pick<WidgetProps, 'level' | 'title'> & { +export type LinksListWidgetProps = Omit< + CollapsibleProps, + 'children' | 'disablePadding' | 'hasBorders' +> & { className?: string; /** * Should the links be ordered? @@ -71,13 +74,7 @@ export const LinksListWidget: FC<LinksListWidgetProps> = ({ )); return ( - <Widget - {...props} - className={styles.widget} - expanded={true} - withBorders={true} - withScroll={true} - > + <Collapsible {...props} className={styles.widget} disablePadding hasBorders> <List className={`${styles.list} ${styles[listKindClass]} ${className}`} hideMarker @@ -85,6 +82,6 @@ export const LinksListWidget: FC<LinksListWidgetProps> = ({ > {getListItems(items)} </List> - </Widget> + </Collapsible> ); }; diff --git a/src/components/organisms/widgets/sharing.stories.tsx b/src/components/organisms/widgets/sharing.stories.tsx index 3f4a79e..d2be621 100644 --- a/src/components/organisms/widgets/sharing.stories.tsx +++ b/src/components/organisms/widgets/sharing.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { Sharing as SharingWidget } from './sharing'; /** @@ -29,36 +29,6 @@ export default { value: {}, }, }, - expanded: { - control: { - type: null, - }, - description: 'Default widget state (expanded or collapsed).', - table: { - category: 'Options', - defaultValue: { summary: true }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - level: { - control: { - type: 'number', - min: 1, - max: 6, - }, - description: 'The heading level.', - table: { - category: 'Options', - defaultValue: { summary: 2 }, - }, - type: { - name: 'number', - required: false, - }, - }, media: { control: { type: null, diff --git a/src/components/organisms/widgets/sharing.tsx b/src/components/organisms/widgets/sharing.tsx index eeffb71..47ec49d 100644 --- a/src/components/organisms/widgets/sharing.tsx +++ b/src/components/organisms/widgets/sharing.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react'; import { useIntl } from 'react-intl'; -import { SharingLink, type SharingMedium } from '../../atoms'; -import { Widget, type WidgetProps } from '../../molecules'; +import { Heading, SharingLink, type SharingMedium } from '../../atoms'; +import { Collapsible, type CollapsibleProps } from '../../molecules'; import styles from './sharing.module.scss'; /** @@ -80,7 +80,7 @@ export type SharingData = { url: string; }; -export type SharingProps = { +export type SharingProps = Omit<CollapsibleProps, 'children' | 'heading'> & { /** * Set additional classnames to the sharing links list. */ @@ -90,14 +90,6 @@ export type SharingProps = { */ data: SharingData; /** - * The widget default state. - */ - expanded?: WidgetProps['expanded']; - /** - * The HTML heading level. - */ - level?: WidgetProps['level']; - /** * A list of active and ordered sharing medium. */ media: SharingMedium[]; @@ -112,8 +104,6 @@ export const Sharing: FC<SharingProps> = ({ className = '', data, media, - expanded = true, - level = 2, ...props }) => { const listClass = `${styles.list} ${className}`; @@ -255,8 +245,15 @@ export const Sharing: FC<SharingProps> = ({ )); return ( - <Widget {...props} expanded={expanded} level={level} title={widgetTitle}> + <Collapsible + {...props} + heading={ + <Heading isFake level={3}> + {widgetTitle} + </Heading> + } + > <ul className={listClass}>{getItems()}</ul> - </Widget> + </Collapsible> ); }; diff --git a/src/components/organisms/widgets/social-media.stories.tsx b/src/components/organisms/widgets/social-media.stories.tsx index 6da3f3a..8064157 100644 --- a/src/components/organisms/widgets/social-media.stories.tsx +++ b/src/components/organisms/widgets/social-media.stories.tsx @@ -1,4 +1,5 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Heading } from '../../atoms'; import { SocialMedia as SocialMediaWidget, type Media } from './social-media'; /** @@ -8,18 +9,6 @@ export default { title: 'Organisms/Widgets', component: SocialMediaWidget, argTypes: { - level: { - control: { - type: 'number', - min: 1, - max: 6, - }, - description: 'The heading level.', - type: { - name: 'number', - required: true, - }, - }, media: { description: 'The links data.', type: { @@ -28,16 +17,6 @@ export default { value: {}, }, }, - title: { - control: { - type: 'text', - }, - description: 'The widget title.', - type: { - name: 'string', - required: true, - }, - }, }, } as ComponentMeta<typeof SocialMediaWidget>; @@ -55,7 +34,10 @@ const media: Media[] = [ */ export const SocialMedia = Template.bind({}); SocialMedia.args = { + heading: ( + <Heading isFake level={3}> + Follow me + </Heading> + ), media, - title: 'Follow me', - level: 2, }; diff --git a/src/components/organisms/widgets/social-media.test.tsx b/src/components/organisms/widgets/social-media.test.tsx index 2cd3afb..ead29d9 100644 --- a/src/components/organisms/widgets/social-media.test.tsx +++ b/src/components/organisms/widgets/social-media.test.tsx @@ -1,6 +1,7 @@ import { describe, expect, it } from '@jest/globals'; import { render, screen as rtlScreen } from '../../../../tests/utils'; import { SocialMedia, type Media } from './social-media'; +import { Heading } from 'src/components/atoms'; const media: Media[] = [ { icon: 'Github', id: 'github', label: 'Github', url: '#' }, @@ -20,7 +21,12 @@ jest.mock('@assets/images/social-media/twitter.svg', () => 'svg-file'); describe('SocialMedia', () => { it('renders the widget title', () => { - render(<SocialMedia media={media} title={title} level={titleLevel} />); + render( + <SocialMedia + heading={<Heading level={titleLevel}>{title}</Heading>} + media={media} + /> + ); expect( rtlScreen.getByRole('heading', { level: titleLevel, @@ -30,7 +36,12 @@ describe('SocialMedia', () => { }); it('renders the correct number of items', () => { - render(<SocialMedia media={media} title={title} level={titleLevel} />); + render( + <SocialMedia + heading={<Heading level={titleLevel}>{title}</Heading>} + media={media} + /> + ); expect(rtlScreen.getAllByRole('listitem')).toHaveLength(media.length); }); }); diff --git a/src/components/organisms/widgets/social-media.tsx b/src/components/organisms/widgets/social-media.tsx index ddeb09c..14c8fe6 100644 --- a/src/components/organisms/widgets/social-media.tsx +++ b/src/components/organisms/widgets/social-media.tsx @@ -1,13 +1,13 @@ import type { FC } from 'react'; import { List, ListItem, SocialLink, type SocialLinkProps } from '../../atoms'; -import { Widget, type WidgetProps } from '../../molecules'; +import { Collapsible, type CollapsibleProps } from '../../molecules'; import styles from './social-media.module.scss'; export type Media = Required< Pick<SocialLinkProps, 'icon' | 'id' | 'label' | 'url'> >; -export type SocialMediaProps = Pick<WidgetProps, 'level' | 'title'> & { +export type SocialMediaProps = Omit<CollapsibleProps, 'children'> & { media: Media[]; }; @@ -31,10 +31,10 @@ export const SocialMedia: FC<SocialMediaProps> = ({ media, ...props }) => { )); return ( - <Widget expanded={true} {...props}> + <Collapsible {...props}> <List className={styles.list} hideMarker isInline spacing="xs"> {getItems(media)} </List> - </Widget> + </Collapsible> ); }; diff --git a/src/components/organisms/widgets/table-of-contents.test.tsx b/src/components/organisms/widgets/table-of-contents.test.tsx index f0917b8..f5b2a87 100644 --- a/src/components/organisms/widgets/table-of-contents.test.tsx +++ b/src/components/organisms/widgets/table-of-contents.test.tsx @@ -1,13 +1,11 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; import { TableOfContents } from './table-of-contents'; describe('TableOfContents', () => { - it('renders the ToC title', () => { + it('renders a title', () => { const divEl = document.createElement('div'); render(<TableOfContents wrapper={divEl} />); - expect( - screen.getByRole('heading', { level: 2, name: /Table of Contents/i }) - ).toBeInTheDocument(); + expect(rtlScreen.getByText(/Table of Contents/i)).toBeInTheDocument(); }); }); diff --git a/src/components/organisms/widgets/table-of-contents.tsx b/src/components/organisms/widgets/table-of-contents.tsx index e67b495..8892485 100644 --- a/src/components/organisms/widgets/table-of-contents.tsx +++ b/src/components/organisms/widgets/table-of-contents.tsx @@ -3,6 +3,7 @@ import { useIntl } from 'react-intl'; import { useHeadingsTree, type Heading } from '../../../utils/hooks'; import { type LinksListItems, LinksListWidget } from './links-list-widget'; import styles from './table-of-contents.module.scss'; +import { Heading as HeadingComponent } from 'src/components/atoms'; type TableOfContentsProps = { /** @@ -43,10 +44,13 @@ export const TableOfContents: FC<TableOfContentsProps> = ({ wrapper }) => { return ( <LinksListWidget className={styles.list} + heading={ + <HeadingComponent isFake level={3}> + {title} + </HeadingComponent> + } isOrdered items={getItems(headingsTree)} - level={2} - title={title} /> ); }; diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx index 68df415..683b6b2 100644 --- a/src/components/templates/page/page-layout.stories.tsx +++ b/src/components/templates/page/page-layout.stories.tsx @@ -252,8 +252,6 @@ SinglePage.args = { 'linkedin', 'twitter', ]} - level={2} - expanded={true} />, ], withToC: true, @@ -330,8 +328,6 @@ Post.args = { 'linkedin', 'twitter', ]} - level={2} - expanded={true} />, ], withToC: true, @@ -372,10 +368,13 @@ Blog.args = { ), widgets: [ <LinksListWidget - key="sidebar-widget1" + heading={ + <Heading isFake level={3}> + Categories + </Heading> + } items={blogCategories} - title="Categories" - level={2} + key="sidebar-widget1" />, ], }; diff --git a/src/components/templates/page/page-layout.test.tsx b/src/components/templates/page/page-layout.test.tsx index d21e14c..6609b48 100644 --- a/src/components/templates/page/page-layout.test.tsx +++ b/src/components/templates/page/page-layout.test.tsx @@ -1,6 +1,6 @@ import { describe, expect, it } from '@jest/globals'; -import { BreadcrumbList } from 'schema-dts'; -import { render, screen } from '../../../../tests/utils'; +import type { BreadcrumbList } from 'schema-dts'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; import { comments } from '../../organisms/layout/comments-list.fixture'; import { PageLayout } from './page-layout'; @@ -25,7 +25,7 @@ describe('PageLayout', () => { </PageLayout> ); expect( - screen.getByRole('heading', { level: 1, name: title }) + rtlScreen.getByRole('heading', { level: 1, name: title }) ).toBeInTheDocument(); }); @@ -39,7 +39,7 @@ describe('PageLayout', () => { {children} </PageLayout> ); - expect(screen.getByText(children)).toBeInTheDocument(); + expect(rtlScreen.getByText(children)).toBeInTheDocument(); }); it('renders the breadcrumb', () => { @@ -53,7 +53,7 @@ describe('PageLayout', () => { </PageLayout> ); expect( - screen.getByRole('navigation', { name: 'Breadcrumb' }) + rtlScreen.getByRole('navigation', { name: 'Breadcrumb' }) ).toBeInTheDocument(); }); @@ -68,9 +68,7 @@ describe('PageLayout', () => { {children} </PageLayout> ); - expect( - screen.getByRole('heading', { level: 2, name: /Table of Contents/i }) - ).toBeInTheDocument(); + expect(rtlScreen.getByText(/Table of Contents/i)).toBeInTheDocument(); }); it('renders the comment form', () => { @@ -85,7 +83,7 @@ describe('PageLayout', () => { </PageLayout> ); expect( - screen.getByRole('form', { name: /Leave a comment/i }) + rtlScreen.getByRole('form', { name: /Leave a comment/i }) ).toBeInTheDocument(); }); @@ -102,7 +100,7 @@ describe('PageLayout', () => { </PageLayout> ); expect( - screen.getByRole('heading', { level: 2, name: /Comments/i }) + rtlScreen.getByRole('heading', { level: 2, name: /Comments/i }) ).toBeInTheDocument(); }); }); |
