diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-04-12 15:48:47 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-04-12 15:48:47 +0200 |
| commit | ff3a251e75fafce7d95177010401483127973313 (patch) | |
| tree | 9a0c9e0ff3f8525bf138287b41ac0527551389fd /src/components | |
| parent | 714273556f5278746a4022d0e87153ff431a61cf (diff) | |
chore: add a MainNavButton component
I also move the active state from the hamburger to this pseudo-button.
It makes more sense that the button handles the icon shape.
Diffstat (limited to 'src/components')
8 files changed, 235 insertions, 39 deletions
diff --git a/src/components/atoms/icons/hamburger.module.scss b/src/components/atoms/icons/hamburger.module.scss index 09e7e30..4fba4df 100644 --- a/src/components/atoms/icons/hamburger.module.scss +++ b/src/components/atoms/icons/hamburger.module.scss @@ -1,16 +1,21 @@ @use "@styles/abstracts/functions" as fun; -.icon { - display: block; +.wrapper { + display: flex; + align-items: center; width: var(--icon-size, #{fun.convert-px(50)}); height: var(--icon-size, #{fun.convert-px(50)}); position: relative; +} +.icon { &, &::before, &::after { display: block; height: fun.convert-px(7); + width: 100%; + position: absolute; background: var(--color-primary-lighter); background-image: linear-gradient( to right, @@ -25,33 +30,13 @@ &::before, &::after { content: ""; - position: absolute; - left: fun.convert-px(-1); - right: fun.convert-px(-1); } &::before { - bottom: fun.convert-px(15); + top: fun.convert-px(-15); } &::after { - top: fun.convert-px(15); - } - - &--active { - background: transparent; - border: transparent; - - &::before { - bottom: 0; - transform-origin: 50% 50%; - transform: rotate(45deg); - } - - &::after { - top: 0; - transform-origin: 50% 50%; - transform: rotate(-45deg); - } + bottom: fun.convert-px(-15); } } diff --git a/src/components/atoms/icons/hamburger.stories.tsx b/src/components/atoms/icons/hamburger.stories.tsx index 062d3ee..c753e69 100644 --- a/src/components/atoms/icons/hamburger.stories.tsx +++ b/src/components/atoms/icons/hamburger.stories.tsx @@ -9,7 +9,7 @@ export default { control: { type: 'text', }, - description: 'Set additional classnames.', + description: 'Set additional classnames to the icon wrapper.', table: { category: 'Styles', }, @@ -18,14 +18,17 @@ export default { required: false, }, }, - isActive: { + iconClassName: { control: { - type: 'boolean', + type: 'text', + }, + description: 'Set additional classnames to the icon.', + table: { + category: 'Styles', }, - description: 'Transform hamburger into a cross when state is active.', type: { - name: 'boolean', - required: true, + name: 'string', + required: false, }, }, }, diff --git a/src/components/atoms/icons/hamburger.test.tsx b/src/components/atoms/icons/hamburger.test.tsx index f8a3c04..7173a23 100644 --- a/src/components/atoms/icons/hamburger.test.tsx +++ b/src/components/atoms/icons/hamburger.test.tsx @@ -3,7 +3,7 @@ import Hamburger from './hamburger'; describe('Hamburger', () => { it('renders a Hamburger icon', () => { - const { container } = render(<Hamburger isActive={false} />); + const { container } = render(<Hamburger />); expect(container).toBeDefined(); }); }); diff --git a/src/components/atoms/icons/hamburger.tsx b/src/components/atoms/icons/hamburger.tsx index 6716b26..7e7c2c9 100644 --- a/src/components/atoms/icons/hamburger.tsx +++ b/src/components/atoms/icons/hamburger.tsx @@ -3,13 +3,14 @@ import styles from './hamburger.module.scss'; type HamburgerProps = { /** - * Set additional classnames to the icon. + * Set additional classnames to the icon wrapper. */ className?: string; + /** - * Transform hamburger to a close icon when active. + * Set additional classnames to the icon. */ - isActive: boolean; + iconClassName?: string; }; /** @@ -17,11 +18,15 @@ type HamburgerProps = { * * Render a Hamburger icon. */ -const Hamburger: FC<HamburgerProps> = ({ className = '', isActive }) => { - const stateClass = isActive ? `${styles['icon--active']}` : ''; - const iconClasses = `${styles.icon} ${stateClass} ${className}`; - - return <span className={iconClasses}></span>; +const Hamburger: FC<HamburgerProps> = ({ + className = '', + iconClassName = '', +}) => { + return ( + <span className={`${styles.wrapper} ${className}`}> + <span className={`${styles.icon} ${iconClassName}`}></span> + </span> + ); }; export default Hamburger; diff --git a/src/components/molecules/buttons/main-nav-button.module.scss b/src/components/molecules/buttons/main-nav-button.module.scss new file mode 100644 index 0000000..bc1ed19 --- /dev/null +++ b/src/components/molecules/buttons/main-nav-button.module.scss @@ -0,0 +1,37 @@ +@use "@styles/abstracts/functions" as fun; + +.checkbox { + position: absolute; + top: calc(#{fun.convert-px(50)} / 2); + left: calc(#{fun.convert-px(50)} / 2); + opacity: 0; + cursor: pointer; +} + +.label { + display: block; + cursor: pointer; + + .icon { + &__wrapper { + --icon-size: #{fun.convert-px(50)}; + } + + &--active { + background: transparent; + border: transparent; + + &::before { + top: 0; + transform-origin: 50% 50%; + transform: rotate(-45deg); + } + + &::after { + bottom: 0; + transform-origin: 50% 50%; + transform: rotate(45deg); + } + } + } +} diff --git a/src/components/molecules/buttons/main-nav-button.stories.tsx b/src/components/molecules/buttons/main-nav-button.stories.tsx new file mode 100644 index 0000000..39e495c --- /dev/null +++ b/src/components/molecules/buttons/main-nav-button.stories.tsx @@ -0,0 +1,80 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { useState } from 'react'; +import { IntlProvider } from 'react-intl'; +import MainNavButtonComponent from './main-nav-button'; + +export default { + title: 'Molecules/Buttons', + component: MainNavButtonComponent, + argTypes: { + checkboxClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the checkbox.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + isActive: { + control: { + type: null, + }, + description: 'The button state.', + type: { + name: 'boolean', + required: true, + }, + }, + labelClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the label.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + setIsActive: { + control: { + type: null, + }, + description: 'A callback function to set the button state.', + type: { + name: 'function', + required: true, + }, + }, + }, +} as ComponentMeta<typeof MainNavButtonComponent>; + +const Template: ComponentStory<typeof MainNavButtonComponent> = ({ + isActive, + setIsActive: _setIsActive, + ...args +}) => { + const [isChecked, setIsChecked] = useState<boolean>(isActive); + + return ( + <IntlProvider locale="en"> + <MainNavButtonComponent + isActive={isChecked} + setIsActive={setIsChecked} + {...args} + /> + </IntlProvider> + ); +}; + +export const MainNavButton = Template.bind({}); +MainNavButton.args = { + isActive: false, +}; diff --git a/src/components/molecules/buttons/main-nav-button.test.tsx b/src/components/molecules/buttons/main-nav-button.test.tsx new file mode 100644 index 0000000..e757305 --- /dev/null +++ b/src/components/molecules/buttons/main-nav-button.test.tsx @@ -0,0 +1,11 @@ +import { render, screen } from '@test-utils'; +import MainNavButton from './main-nav-button'; + +describe('MainNavButton', () => { + it('renders a checkbox', () => { + render(<MainNavButton isActive={false} setIsActive={() => null} />); + expect( + screen.getByRole('checkbox', { name: 'Open menu' }) + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/molecules/buttons/main-nav-button.tsx b/src/components/molecules/buttons/main-nav-button.tsx new file mode 100644 index 0000000..59407db --- /dev/null +++ b/src/components/molecules/buttons/main-nav-button.tsx @@ -0,0 +1,75 @@ +import Checkbox, { CheckboxProps } from '@components/atoms/forms/checkbox'; +import Label from '@components/atoms/forms/label'; +import Hamburger from '@components/atoms/icons/hamburger'; +import { VFC } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './main-nav-button.module.scss'; + +export type MainNavButtonProps = { + /** + * Set additional classnames to the checkbox. + */ + checkboxClassName?: string; + /** + * The button state. + */ + isActive: CheckboxProps['value']; + /** + * Set additional classnames to the label. + */ + labelClassName?: string; + /** + * A callback function to handle button state. + */ + setIsActive: CheckboxProps['setValue']; +}; + +/** + * MainNavButton component + * + * Render a hamburger icon or a close icon depending on state. + */ +const MainNavButton: VFC<MainNavButtonProps> = ({ + checkboxClassName = '', + isActive, + labelClassName = '', + setIsActive, +}) => { + const intl = useIntl(); + const label = isActive + ? intl.formatMessage({ + defaultMessage: 'Close menu', + id: 'wT7YZb', + description: 'MainNavButton: close menu label', + }) + : intl.formatMessage({ + defaultMessage: 'Open menu', + id: 'P7j8ZZ', + description: 'MainNavButton: open menu label', + }); + const hamburgerModifier = isActive ? 'icon--active' : ''; + + return ( + <> + <Checkbox + id="main-nav-button" + name="main-nav-button" + value={isActive} + setValue={setIsActive} + className={`${styles.checkbox} ${checkboxClassName}`} + /> + <Label + htmlFor="main-nav-button" + className={`${styles.label} ${labelClassName}`} + aria-label={label} + > + <Hamburger + className={styles.icon__wrapper} + iconClassName={styles[hamburgerModifier]} + /> + </Label> + </> + ); +}; + +export default MainNavButton; |
