diff options
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; |
