aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/molecules/buttons/heading-button.module.scss41
-rw-r--r--src/components/molecules/buttons/heading-button.stories.tsx75
-rw-r--r--src/components/molecules/buttons/heading-button.test.tsx32
-rw-r--r--src/components/molecules/buttons/heading-button.tsx66
4 files changed, 214 insertions, 0 deletions
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..d068001
--- /dev/null
+++ b/src/components/molecules/buttons/heading-button.module.scss
@@ -0,0 +1,41 @@
+@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;
+ 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;
+
+ &:hover,
+ &:focus {
+ .icon {
+ background: var(--color-primary-light);
+ color: var(--color-fg-inverted);
+ transform: scale(1.25);
+
+ &::before,
+ &::after {
+ background: var(--color-bg);
+ }
+ }
+ }
+}
+
+.heading {
+ background: none;
+ padding: var(--spacing-2xs) 0;
+}
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..0a23b08
--- /dev/null
+++ b/src/components/molecules/buttons/heading-button.stories.tsx
@@ -0,0 +1,75 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import { IntlProvider } from 'react-intl';
+import HeadingButtonComponent from './heading-button';
+
+export default {
+ title: 'Molecules/Buttons',
+ component: HeadingButtonComponent,
+ argTypes: {
+ expanded: {
+ control: {
+ type: null,
+ },
+ description: 'Heading button state (plus or minus).',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ level: {
+ control: {
+ type: 'number',
+ },
+ 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 (
+ <IntlProvider locale="en">
+ <HeadingButtonComponent
+ expanded={isExpanded}
+ setExpanded={setIsExpanded}
+ {...args}
+ />
+ </IntlProvider>
+ );
+};
+
+export const HeadingButton = Template.bind({});
+HeadingButton.args = {
+ 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..700b3e1
--- /dev/null
+++ b/src/components/molecules/buttons/heading-button.tsx
@@ -0,0 +1,66 @@
+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'> & {
+ /**
+ * 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> = ({
+ 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}
+ onClick={() => setExpanded(!expanded)}
+ >
+ <Heading
+ level={level}
+ withMargin={false}
+ additionalClasses={styles.heading}
+ >
+ <span className="screen-reader-text">{titlePrefix} </span>
+ {title}
+ </Heading>
+ <PlusMinus state={iconState} additionalClasses={styles.icon} />
+ </button>
+ );
+};
+
+export default HeadingButton;