diff options
Diffstat (limited to 'src/components/molecules/buttons')
12 files changed, 477 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..9721bff --- /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(44)}, 6vw, #{fun.convert-px(55)}); + height: clamp(#{fun.convert-px(44)}, 6vw, #{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..fe58293 --- /dev/null +++ b/src/components/molecules/buttons/back-to-top.stories.tsx @@ -0,0 +1,44 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { IntlProvider } from 'react-intl'; +import BackToTopComponent from './back-to-top'; + +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) => ( + <IntlProvider locale="en"> + <BackToTopComponent {...args} /> + </IntlProvider> +); + +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..8a52231 --- /dev/null +++ b/src/components/molecules/buttons/back-to-top.tsx @@ -0,0 +1,45 @@ +import ButtonLink from '@components/atoms/buttons/button-link'; +import Arrow from '@components/atoms/icons/arrow'; +import { VFC } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './back-to-top.module.scss'; + +export type BackToTopProps = { + /** + * Set additional classnames to the button wrapper. + */ + className?: string; + /** + * An element id (without hashtag) to use as anchor. + */ + target: string; +}; + +/** + * BackToTop component + * + * Render a back to top link. + */ +const BackToTop: VFC<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..1d16410 --- /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 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; + + &: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..fc79749 --- /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 { 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; + /** + * 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: VFC<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..cfc1b0b --- /dev/null +++ b/src/components/molecules/buttons/help-button.stories.tsx @@ -0,0 +1,44 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { IntlProvider } from 'react-intl'; +import HelpButtonComponent from './help-button'; + +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) => ( + <IntlProvider locale="en"> + <HelpButtonComponent {...args} /> + </IntlProvider> +); + +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..aeb84ec --- /dev/null +++ b/src/components/molecules/buttons/help-button.tsx @@ -0,0 +1,38 @@ +import Button, { ButtonProps } from '@components/atoms/buttons/button'; +import { VFC } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './help-button.module.scss'; + +export type HelpButtonProps = Pick<ButtonProps, 'onClick'> & { + /** + * Set additional classnames to the button wrapper. + */ + className?: string; +}; + +/** + * HelpButton component + * + * Render a button with an interrogation mark icon. + */ +const HelpButton: VFC<HelpButtonProps> = ({ className = '', onClick }) => { + const intl = useIntl(); + const text = intl.formatMessage({ + defaultMessage: 'Help', + id: 'i+/ckF', + description: 'HelpButton: screen reader text', + }); + + return ( + <Button + shape="circle" + className={`${styles.btn} ${className}`} + onClick={onClick} + > + <span className="screen-reader-text">{text}</span> + <span className={styles.icon}>?</span> + </Button> + ); +}; + +export default HelpButton; |
