summaryrefslogtreecommitdiffstats
path: root/src/components/molecules/buttons
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/molecules/buttons')
-rw-r--r--src/components/molecules/buttons/back-to-top.module.scss51
-rw-r--r--src/components/molecules/buttons/back-to-top.stories.tsx47
-rw-r--r--src/components/molecules/buttons/back-to-top.test.tsx10
-rw-r--r--src/components/molecules/buttons/back-to-top.tsx43
-rw-r--r--src/components/molecules/buttons/heading-button.module.scss44
-rw-r--r--src/components/molecules/buttons/heading-button.stories.tsx105
-rw-r--r--src/components/molecules/buttons/heading-button.test.tsx32
-rw-r--r--src/components/molecules/buttons/heading-button.tsx67
-rw-r--r--src/components/molecules/buttons/help-button.module.scss21
-rw-r--r--src/components/molecules/buttons/help-button.stories.tsx47
-rw-r--r--src/components/molecules/buttons/help-button.test.tsx9
-rw-r--r--src/components/molecules/buttons/help-button.tsx37
12 files changed, 513 insertions, 0 deletions
diff --git a/src/components/molecules/buttons/back-to-top.module.scss b/src/components/molecules/buttons/back-to-top.module.scss
new file mode 100644
index 0000000..77ee97b
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top.module.scss
@@ -0,0 +1,51 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+ .link {
+ width: clamp(#{fun.convert-px(48)}, 8vw, #{fun.convert-px(55)});
+ height: clamp(#{fun.convert-px(48)}, 8vw, #{fun.convert-px(55)});
+
+ svg {
+ width: 100%;
+ }
+
+ :global {
+ .arrow-head {
+ transform: translateY(30%) scale(1.2);
+ transition: all 0.45s ease-in-out 0s;
+ }
+
+ .arrow-bar {
+ opacity: 0;
+ transform: translateY(30%) scaleY(0);
+ transition: transform 0.45s ease-in-out 0s, opacity 0.1s linear 0.2s;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ :global {
+ .arrow-head {
+ transform: translateY(0) scale(1);
+ }
+
+ .arrow-bar {
+ opacity: 1;
+ transform: translateY(0) scaleY(1);
+ }
+ }
+
+ svg {
+ :global {
+ animation: pulse 1.2s ease-in-out 0.6s infinite;
+ }
+ }
+ }
+
+ &:active {
+ svg {
+ animation-play-state: paused;
+ }
+ }
+ }
+}
diff --git a/src/components/molecules/buttons/back-to-top.stories.tsx b/src/components/molecules/buttons/back-to-top.stories.tsx
new file mode 100644
index 0000000..a338b8b
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top.stories.tsx
@@ -0,0 +1,47 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import BackToTopComponent from './back-to-top';
+
+/**
+ * BackToTop - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Buttons',
+ component: BackToTopComponent,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the button wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ target: {
+ control: {
+ type: 'text',
+ },
+ description: 'An element id (without hashtag) to use as anchor.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof BackToTopComponent>;
+
+const Template: ComponentStory<typeof BackToTopComponent> = (args) => (
+ <BackToTopComponent {...args} />
+);
+
+/**
+ * Buttons Stories - Back to top
+ */
+export const BackToTop = Template.bind({});
+BackToTop.args = {
+ target: 'top',
+};
diff --git a/src/components/molecules/buttons/back-to-top.test.tsx b/src/components/molecules/buttons/back-to-top.test.tsx
new file mode 100644
index 0000000..2b3a0a9
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top.test.tsx
@@ -0,0 +1,10 @@
+import { render, screen } from '@test-utils';
+import BackToTop from './back-to-top';
+
+describe('BackToTop', () => {
+ it('renders a BackToTop link', () => {
+ render(<BackToTop target="top" />);
+ expect(screen.getByRole('link')).toHaveAccessibleName('Back to top');
+ expect(screen.getByRole('link')).toHaveAttribute('href', '/#top');
+ });
+});
diff --git a/src/components/molecules/buttons/back-to-top.tsx b/src/components/molecules/buttons/back-to-top.tsx
new file mode 100644
index 0000000..bd1925a
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top.tsx
@@ -0,0 +1,43 @@
+import ButtonLink, {
+ type ButtonLinkProps,
+} from '@components/atoms/buttons/button-link';
+import Arrow from '@components/atoms/icons/arrow';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './back-to-top.module.scss';
+
+export type BackToTopProps = Pick<ButtonLinkProps, 'target'> & {
+ /**
+ * Set additional classnames to the button wrapper.
+ */
+ className?: string;
+};
+
+/**
+ * BackToTop component
+ *
+ * Render a back to top link.
+ */
+const BackToTop: FC<BackToTopProps> = ({ className = '', target }) => {
+ const intl = useIntl();
+ const linkName = intl.formatMessage({
+ defaultMessage: 'Back to top',
+ description: 'BackToTop: link text',
+ id: 'm+SUSR',
+ });
+
+ return (
+ <div className={`${styles.wrapper} ${className}`}>
+ <ButtonLink
+ shape="square"
+ target={`#${target}`}
+ aria-label={linkName}
+ className={styles.link}
+ >
+ <Arrow direction="top" />
+ </ButtonLink>
+ </div>
+ );
+};
+
+export default BackToTop;
diff --git a/src/components/molecules/buttons/heading-button.module.scss b/src/components/molecules/buttons/heading-button.module.scss
new file mode 100644
index 0000000..3c69221
--- /dev/null
+++ b/src/components/molecules/buttons/heading-button.module.scss
@@ -0,0 +1,44 @@
+@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
new file mode 100644
index 0000000..59f7be9
--- /dev/null
+++ b/src/components/molecules/buttons/heading-button.stories.tsx
@@ -0,0 +1,105 @@
+import headingStories from '@components/atoms/headings/heading.stories';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import 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
new file mode 100644
index 0000000..be3865a
--- /dev/null
+++ b/src/components/molecules/buttons/heading-button.test.tsx
@@ -0,0 +1,32 @@
+import { render, screen } from '@test-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
new file mode 100644
index 0000000..0ed9a76
--- /dev/null
+++ b/src/components/molecules/buttons/heading-button.tsx
@@ -0,0 +1,67 @@
+import Heading, { type HeadingProps } from '@components/atoms/headings/heading';
+import PlusMinus from '@components/atoms/icons/plus-minus';
+import { FC, SetStateAction } 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;
+ /**
+ * 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.
+ */
+const HeadingButton: FC<HeadingButtonProps> = ({
+ className = '',
+ expanded,
+ level,
+ setExpanded,
+ title,
+}) => {
+ const intl = useIntl();
+ 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',
+ });
+
+ return (
+ <button
+ type="button"
+ className={`${styles.wrapper} ${className}`}
+ onClick={() => setExpanded(!expanded)}
+ >
+ <Heading level={level} withMargin={false} className={styles.heading}>
+ <span className="screen-reader-text">{titlePrefix} </span>
+ {title}
+ </Heading>
+ <PlusMinus state={iconState} className={styles.icon} />
+ </button>
+ );
+};
+
+export default HeadingButton;
diff --git a/src/components/molecules/buttons/help-button.module.scss b/src/components/molecules/buttons/help-button.module.scss
new file mode 100644
index 0000000..42d49f6
--- /dev/null
+++ b/src/components/molecules/buttons/help-button.module.scss
@@ -0,0 +1,21 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.btn {
+ padding: var(--spacing-xs);
+
+ &:not(:disabled) {
+ &:focus {
+ text-decoration: none;
+ }
+ }
+
+ @include mix.pointer("fine") {
+ padding: var(--spacing-2xs);
+ }
+}
+
+.icon {
+ color: var(--color-primary-dark);
+ font-weight: 600;
+}
diff --git a/src/components/molecules/buttons/help-button.stories.tsx b/src/components/molecules/buttons/help-button.stories.tsx
new file mode 100644
index 0000000..4968b27
--- /dev/null
+++ b/src/components/molecules/buttons/help-button.stories.tsx
@@ -0,0 +1,47 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import HelpButtonComponent from './help-button';
+
+/**
+ * HelpButton - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Buttons',
+ component: HelpButtonComponent,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the button wrapper.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ onClick: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to handle click on button.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof HelpButtonComponent>;
+
+const Template: ComponentStory<typeof HelpButtonComponent> = (args) => (
+ <HelpButtonComponent {...args} />
+);
+
+/**
+ * Help Button Stories - Level 1
+ */
+export const HelpButton = Template.bind({});
diff --git a/src/components/molecules/buttons/help-button.test.tsx b/src/components/molecules/buttons/help-button.test.tsx
new file mode 100644
index 0000000..78987ef
--- /dev/null
+++ b/src/components/molecules/buttons/help-button.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import HelpButton from './help-button';
+
+describe('Help', () => {
+ it('renders a help button', () => {
+ render(<HelpButton />);
+ expect(screen.getByRole('button', { name: 'Help ?' })).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/buttons/help-button.tsx b/src/components/molecules/buttons/help-button.tsx
new file mode 100644
index 0000000..ccf1ebd
--- /dev/null
+++ b/src/components/molecules/buttons/help-button.tsx
@@ -0,0 +1,37 @@
+import Button, { type ButtonProps } from '@components/atoms/buttons/button';
+import { forwardRef, ForwardRefRenderFunction } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './help-button.module.scss';
+
+export type HelpButtonProps = Pick<ButtonProps, 'className' | 'onClick'>;
+
+/**
+ * HelpButton component
+ *
+ * Render a button with an interrogation mark icon.
+ */
+const HelpButton: ForwardRefRenderFunction<
+ HTMLButtonElement,
+ HelpButtonProps
+> = ({ className = '', ...props }, ref) => {
+ const intl = useIntl();
+ const text = intl.formatMessage({
+ defaultMessage: 'Help',
+ id: 'i+/ckF',
+ description: 'HelpButton: screen reader text',
+ });
+
+ return (
+ <Button
+ className={`${styles.btn} ${className}`}
+ ref={ref}
+ shape="circle"
+ {...props}
+ >
+ <span className="screen-reader-text">{text}</span>
+ <span className={styles.icon}>?</span>
+ </Button>
+ );
+};
+
+export default forwardRef(HelpButton);