diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-04-15 15:34:21 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-04-15 16:26:29 +0200 |
| commit | 6ec16bc15cc78e62cb94e131699625fa5363437c (patch) | |
| tree | 8090c2a611fbec2e79f49422c1ca582fa33c5248 | |
| parent | 64570357f9608ad6638b1f8cc283ee9dd1cc3264 (diff) | |
chore: add a Widget component
| -rw-r--r-- | src/components/molecules/buttons/heading-button.module.scss | 2 | ||||
| -rw-r--r-- | src/components/molecules/buttons/heading-button.tsx | 19 | ||||
| -rw-r--r-- | src/components/molecules/layout/widget.module.scss | 40 | ||||
| -rw-r--r-- | src/components/molecules/layout/widget.stories.tsx | 85 | ||||
| -rw-r--r-- | src/components/molecules/layout/widget.test.tsx | 19 | ||||
| -rw-r--r-- | src/components/molecules/layout/widget.tsx | 54 |
6 files changed, 209 insertions, 10 deletions
diff --git a/src/components/molecules/buttons/heading-button.module.scss b/src/components/molecules/buttons/heading-button.module.scss index d068001..1d16410 100644 --- a/src/components/molecules/buttons/heading-button.module.scss +++ b/src/components/molecules/buttons/heading-button.module.scss @@ -11,7 +11,7 @@ justify-content: space-between; gap: var(--spacing-md); width: 100%; - padding: 0; + padding: 0 var(--spacing-2xs); position: sticky; top: 0; background: inherit; diff --git a/src/components/molecules/buttons/heading-button.tsx b/src/components/molecules/buttons/heading-button.tsx index 700b3e1..fc79749 100644 --- a/src/components/molecules/buttons/heading-button.tsx +++ b/src/components/molecules/buttons/heading-button.tsx @@ -1,11 +1,15 @@ import Heading, { type HeadingProps } from '@components/atoms/headings/heading'; import PlusMinus from '@components/atoms/icons/plus-minus'; -import { FC, SetStateAction } from 'react'; +import { SetStateAction, VFC } from 'react'; import { useIntl } from 'react-intl'; 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; @@ -24,7 +28,8 @@ export type HeadingButtonProps = Pick<HeadingProps, 'level'> & { * * Render a button as accordion title to toggle body. */ -const HeadingButton: FC<HeadingButtonProps> = ({ +const HeadingButton: VFC<HeadingButtonProps> = ({ + className = '', expanded, level, setExpanded, @@ -47,18 +52,14 @@ const HeadingButton: FC<HeadingButtonProps> = ({ return ( <button type="button" - className={styles.wrapper} + className={`${styles.wrapper} ${className}`} onClick={() => setExpanded(!expanded)} > - <Heading - level={level} - withMargin={false} - additionalClasses={styles.heading} - > + <Heading level={level} withMargin={false} className={styles.heading}> <span className="screen-reader-text">{titlePrefix} </span> {title} </Heading> - <PlusMinus state={iconState} additionalClasses={styles.icon} /> + <PlusMinus state={iconState} className={styles.icon} /> </button> ); }; diff --git a/src/components/molecules/layout/widget.module.scss b/src/components/molecules/layout/widget.module.scss new file mode 100644 index 0000000..727ffb7 --- /dev/null +++ b/src/components/molecules/layout/widget.module.scss @@ -0,0 +1,40 @@ +@use "@styles/abstracts/functions" as fun; + +.widget { + display: flex; + flex-flow: column; + + &__header { + background: var(--color-bg); + } + + &--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 & { + &__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 new file mode 100644 index 0000000..d79f66e --- /dev/null +++ b/src/components/molecules/layout/widget.stories.tsx @@ -0,0 +1,85 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { IntlProvider } from 'react-intl'; +import WidgetComponent from './widget'; + +export default { + title: 'Molecules/Layout', + component: WidgetComponent, + args: { + expanded: true, + withBorders: false, + }, + argTypes: { + children: { + control: { + type: 'text', + }, + description: 'The widget body', + type: { + name: 'string', + required: true, + }, + }, + expanded: { + control: { + type: 'boolean', + }, + description: 'The widget state (expanded or collapsed)', + table: { + category: 'Options', + defaultValue: { summary: true }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + level: { + control: { + type: 'number', + }, + description: 'The heading level.', + type: { + name: 'number', + required: true, + }, + }, + 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, + }, + }, + }, +} as ComponentMeta<typeof WidgetComponent>; + +const Template: ComponentStory<typeof WidgetComponent> = (args) => ( + <IntlProvider locale="en"> + <WidgetComponent {...args} /> + </IntlProvider> +); + +export const Widget = Template.bind({}); +Widget.args = { + children: 'Widget body', + level: 2, + title: 'Widget title', +}; diff --git a/src/components/molecules/layout/widget.test.tsx b/src/components/molecules/layout/widget.test.tsx new file mode 100644 index 0000000..af561ea --- /dev/null +++ b/src/components/molecules/layout/widget.test.tsx @@ -0,0 +1,19 @@ +import { render, screen } from '@test-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 new file mode 100644 index 0000000..c04362a --- /dev/null +++ b/src/components/molecules/layout/widget.tsx @@ -0,0 +1,54 @@ +import { FC, useState } from 'react'; +import HeadingButton, { HeadingButtonProps } from '../buttons/heading-button'; +import styles from './widget.module.scss'; + +export type WidgetProps = Pick< + HeadingButtonProps, + 'expanded' | 'level' | 'title' +> & { + /** + * Set additional classnames to the widget wrapper. + */ + className?: string; + /** + * Determine if the widget body should have borders. Default: false. + */ + withBorders?: boolean; +}; + +/** + * Widget component + * + * Render an expandable widget. + */ +const Widget: FC<WidgetProps> = ({ + children, + className = '', + expanded = true, + level, + title, + withBorders = false, +}) => { + const [isExpanded, setIsExpanded] = useState<boolean>(expanded); + const stateClass = isExpanded ? 'widget--expanded' : 'widget--collapsed'; + const bordersClass = withBorders + ? 'widget--has-borders' + : 'widget--no-borders'; + + return ( + <div + className={`${styles.widget} ${styles[bordersClass]} ${styles[stateClass]} ${className}`} + > + <HeadingButton + level={level} + title={title} + expanded={isExpanded} + setExpanded={setIsExpanded} + className={styles.widget__header} + /> + <div className={styles.widget__body}>{children}</div> + </div> + ); +}; + +export default Widget; |
