aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-04-15 15:34:21 +0200
committerArmand Philippot <git@armandphilippot.com>2022-04-15 16:26:29 +0200
commit6ec16bc15cc78e62cb94e131699625fa5363437c (patch)
tree8090c2a611fbec2e79f49422c1ca582fa33c5248 /src
parent64570357f9608ad6638b1f8cc283ee9dd1cc3264 (diff)
chore: add a Widget component
Diffstat (limited to 'src')
-rw-r--r--src/components/molecules/buttons/heading-button.module.scss2
-rw-r--r--src/components/molecules/buttons/heading-button.tsx19
-rw-r--r--src/components/molecules/layout/widget.module.scss40
-rw-r--r--src/components/molecules/layout/widget.stories.tsx85
-rw-r--r--src/components/molecules/layout/widget.test.tsx19
-rw-r--r--src/components/molecules/layout/widget.tsx54
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;