diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-25 17:23:53 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:15:27 +0100 |
| commit | c21a137e1991af1331fe5768fc6bac15ea9230b1 (patch) | |
| tree | 80569408dbed888273a15d9ae543f553f2798a9b /src | |
| parent | 73e12fe8ae059ef70bbdf8716af421cb72aec76c (diff) | |
refactor(components): extract MainNav component from toolbar
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/organisms/nav/index.ts | 1 | ||||
| -rw-r--r-- | src/components/organisms/nav/main-nav/index.ts | 1 | ||||
| -rw-r--r-- | src/components/organisms/nav/main-nav/main-nav.module.scss | 40 | ||||
| -rw-r--r-- | src/components/organisms/nav/main-nav/main-nav.stories.tsx | 71 | ||||
| -rw-r--r-- | src/components/organisms/nav/main-nav/main-nav.test.tsx | 18 | ||||
| -rw-r--r-- | src/components/organisms/nav/main-nav/main-nav.tsx | 47 | ||||
| -rw-r--r-- | src/components/organisms/toolbar/index.ts | 3 | ||||
| -rw-r--r-- | src/components/organisms/toolbar/main-nav.stories.tsx | 34 | ||||
| -rw-r--r-- | src/components/organisms/toolbar/main-nav.test.tsx | 37 | ||||
| -rw-r--r-- | src/components/organisms/toolbar/main-nav.tsx | 54 | ||||
| -rw-r--r-- | src/components/organisms/toolbar/toolbar.tsx | 28 |
11 files changed, 253 insertions, 81 deletions
diff --git a/src/components/organisms/nav/index.ts b/src/components/organisms/nav/index.ts index ad899e0..957a64a 100644 --- a/src/components/organisms/nav/index.ts +++ b/src/components/organisms/nav/index.ts @@ -1,2 +1,3 @@ export * from './breadcrumbs'; +export * from './main-nav'; export * from './pagination'; diff --git a/src/components/organisms/nav/main-nav/index.ts b/src/components/organisms/nav/main-nav/index.ts new file mode 100644 index 0000000..6b3662f --- /dev/null +++ b/src/components/organisms/nav/main-nav/index.ts @@ -0,0 +1 @@ +export * from './main-nav'; diff --git a/src/components/organisms/nav/main-nav/main-nav.module.scss b/src/components/organisms/nav/main-nav/main-nav.module.scss new file mode 100644 index 0000000..3f94678 --- /dev/null +++ b/src/components/organisms/nav/main-nav/main-nav.module.scss @@ -0,0 +1,40 @@ +@use "../../../../styles/abstracts/functions" as fun; +@use "../../../../styles/abstracts/mixins" as mix; + +.modal { + @include mix.dimensions("md") { + padding: 0; + background: transparent; + border: none; + box-shadow: none; + } +} + +.checkbox { + &:not(:checked) { + ~ .modal { + opacity: 0; + visibility: hidden; + + @include mix.media("screen") { + @include mix.dimensions(null, "sm") { + transform: translateX(-100vw); + } + + @include mix.dimensions("sm") { + transform: perspective(#{fun.convert-px(400)}) + translate3d(0, 0, #{fun.convert-px(-400)}); + transform-origin: 100% -50%; + } + } + + @include mix.media("screen") { + @include mix.dimensions("md") { + opacity: 1; + visibility: visible; + transform: none; + } + } + } + } +} diff --git a/src/components/organisms/nav/main-nav/main-nav.stories.tsx b/src/components/organisms/nav/main-nav/main-nav.stories.tsx new file mode 100644 index 0000000..6333f2c --- /dev/null +++ b/src/components/organisms/nav/main-nav/main-nav.stories.tsx @@ -0,0 +1,71 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Icon } from '../../../atoms'; +import { MainNav } from './main-nav'; + +/** + * MainNav - Storybook Meta + */ +export default { + title: 'Organisms/Nav/MainNav', + component: MainNav, + argTypes: { + items: { + description: 'The main nav items.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + }, +} as ComponentMeta<typeof MainNav>; + +const Template: ComponentStory<typeof MainNav> = (args) => ( + <MainNav {...args} /> +); + +/** + * MainNav Stories - Default + */ +export const Default = Template.bind({}); +Default.args = { + items: [ + { id: 'home', label: 'Home', href: '#home' }, + { id: 'blog', label: 'Blog', href: '#blog' }, + { id: 'projects', label: 'Projects', href: '#projects' }, + { id: 'contact', label: 'Contact', href: '#contact' }, + ], +}; + +/** + * MainNav Stories - WithLogo + */ +export const WithLogo = Template.bind({}); +WithLogo.args = { + items: [ + { + id: 'home', + label: 'Home', + href: '#home', + logo: <Icon aria-hidden shape="home" />, + }, + { + id: 'blog', + label: 'Blog', + href: '#blog', + logo: <Icon aria-hidden shape="posts-stack" />, + }, + { + id: 'projects', + label: 'Projects', + href: '#projects', + logo: <Icon aria-hidden shape="computer" />, + }, + { + id: 'contact', + label: 'Contact', + href: '#contact', + logo: <Icon aria-hidden shape="envelop" />, + }, + ], +}; diff --git a/src/components/organisms/nav/main-nav/main-nav.test.tsx b/src/components/organisms/nav/main-nav/main-nav.test.tsx new file mode 100644 index 0000000..86c1eb5 --- /dev/null +++ b/src/components/organisms/nav/main-nav/main-nav.test.tsx @@ -0,0 +1,18 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { MainNav } from './main-nav'; + +const items = [ + { id: 'home', label: 'Home', href: '#home' }, + { id: 'blog', label: 'Blog', href: '#blog' }, + { id: 'projects', label: 'Projects', href: '#projects' }, + { id: 'contact', label: 'Contact', href: '#contact' }, +]; + +describe('MainNav', () => { + it('renders a list of nav items', () => { + render(<MainNav items={items} />); + + expect(rtlScreen.getAllByRole('link')).toHaveLength(items.length); + }); +}); diff --git a/src/components/organisms/nav/main-nav/main-nav.tsx b/src/components/organisms/nav/main-nav/main-nav.tsx new file mode 100644 index 0000000..5a19399 --- /dev/null +++ b/src/components/organisms/nav/main-nav/main-nav.tsx @@ -0,0 +1,47 @@ +import { + type ForwardRefRenderFunction, + type ReactNode, + forwardRef, +} from 'react'; +import { Nav, type NavProps } from '../../../atoms'; +import { NavItem, NavLink, NavList } from '../../../molecules'; + +export type MainNavItem = { + id: string; + href: string; + label: string; + logo?: ReactNode; +}; + +export type MainNavProps = Omit<NavProps, 'children'> & { + /** + * The main nav items. + */ + items: MainNavItem[]; +}; + +const MainNavWithRef: ForwardRefRenderFunction<HTMLElement, MainNavProps> = ( + { className = '', items, ...props }, + ref +) => { + const wrapperClass = `${className}`; + + return ( + <Nav {...props} className={wrapperClass} ref={ref}> + <NavList isInline spacing="2xs"> + {items.map(({ id, ...link }) => ( + <NavItem key={id}> + <NavLink {...link} isStack variant="main" /> + </NavItem> + ))} + </NavList> + </Nav> + ); +}; + +/** + * MainNav component + * + * Render the main navigation. + */ +export const MainNav = forwardRef(MainNavWithRef); diff --git a/src/components/organisms/toolbar/index.ts b/src/components/organisms/toolbar/index.ts index 9433412..316a52a 100644 --- a/src/components/organisms/toolbar/index.ts +++ b/src/components/organisms/toolbar/index.ts @@ -1,4 +1 @@ -export * from './main-nav'; -export * from './search'; -export * from './settings'; export * from './toolbar'; diff --git a/src/components/organisms/toolbar/main-nav.stories.tsx b/src/components/organisms/toolbar/main-nav.stories.tsx index 57485d3..d79addf 100644 --- a/src/components/organisms/toolbar/main-nav.stories.tsx +++ b/src/components/organisms/toolbar/main-nav.stories.tsx @@ -1,13 +1,13 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { useState } from 'react'; -import { MainNav } from './main-nav'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { useCallback, useState } from 'react'; +import { MainNavItem } from './main-nav'; /** - * MainNav - Storybook Meta + * MainNavItem - Storybook Meta */ export default { - title: 'Organisms/Toolbar/MainNav', - component: MainNav, + title: 'Organisms/Toolbar/MainNavItem', + component: MainNavItem, argTypes: { className: { control: { @@ -54,28 +54,24 @@ export default { }, }, }, -} as ComponentMeta<typeof MainNav>; +} as ComponentMeta<typeof MainNavItem>; -const Template: ComponentStory<typeof MainNav> = ({ +const Template: ComponentStory<typeof MainNavItem> = ({ isActive = false, setIsActive: _setIsActive, ...args }) => { const [isOpen, setIsOpen] = useState<boolean>(isActive); - return ( - <MainNav - isActive={isOpen} - setIsActive={() => { - setIsOpen(!isOpen); - }} - {...args} - /> - ); + const toggle = useCallback(() => { + setIsOpen((prevState) => !prevState); + }, []); + + return <MainNavItem isActive={isOpen} setIsActive={toggle} {...args} />; }; /** - * MainNav Stories - Inactive + * MainNavItem Stories - Inactive */ export const Inactive = Template.bind({}); Inactive.args = { @@ -87,7 +83,7 @@ Inactive.args = { }; /** - * MainNav Stories - Active + * MainNavItem Stories - Active */ export const Active = Template.bind({}); Active.args = { diff --git a/src/components/organisms/toolbar/main-nav.test.tsx b/src/components/organisms/toolbar/main-nav.test.tsx index 054a14e..177e692 100644 --- a/src/components/organisms/toolbar/main-nav.test.tsx +++ b/src/components/organisms/toolbar/main-nav.test.tsx @@ -1,6 +1,10 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { MainNav } from './main-nav'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; +import { MainNavItem } from './main-nav'; + +const doNothing = () => { + // do nothing +}; const items = [ { id: 'home', label: 'Home', href: '/' }, @@ -8,27 +12,34 @@ const items = [ { id: 'contact', label: 'Contact', href: '/contact' }, ]; -describe('MainNav', () => { +describe('MainNavItem', () => { it('renders a checkbox to open main nav', () => { - render(<MainNav items={items} isActive={false} setIsActive={() => null} />); - expect(screen.getByRole('checkbox')).toHaveAccessibleName('Open menu'); + render( + <MainNavItem items={items} isActive={false} setIsActive={doNothing} /> + ); + expect(rtlScreen.getByRole('checkbox')).toHaveAccessibleName('Open menu'); }); it('renders a checkbox to close main nav', () => { - render(<MainNav items={items} isActive={true} setIsActive={() => null} />); - expect(screen.getByRole('checkbox')).toHaveAccessibleName('Close menu'); + render( + <MainNavItem items={items} isActive={true} setIsActive={doNothing} /> + ); + expect(rtlScreen.getByRole('checkbox')).toHaveAccessibleName('Close menu'); }); it('renders the correct number of items', () => { - render(<MainNav items={items} isActive={true} setIsActive={() => null} />); - expect(screen.getAllByRole('listitem')).toHaveLength(items.length); + render( + <MainNavItem items={items} isActive={true} setIsActive={doNothing} /> + ); + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(items.length); }); it('renders some links with the right label', () => { - render(<MainNav items={items} isActive={true} setIsActive={() => null} />); - expect(screen.getByRole('link', { name: items[0].label })).toHaveAttribute( - 'href', - items[0].href + render( + <MainNavItem items={items} isActive={true} setIsActive={doNothing} /> ); + expect( + rtlScreen.getByRole('link', { name: items[0].label }) + ).toHaveAttribute('href', items[0].href); }); }); diff --git a/src/components/organisms/toolbar/main-nav.tsx b/src/components/organisms/toolbar/main-nav.tsx index a5a105e..ee799f5 100644 --- a/src/components/organisms/toolbar/main-nav.tsx +++ b/src/components/organisms/toolbar/main-nav.tsx @@ -1,28 +1,11 @@ -import { - forwardRef, - type ReactNode, - type ForwardRefRenderFunction, -} from 'react'; +import { forwardRef, type ForwardRefRenderFunction } from 'react'; import { useIntl } from 'react-intl'; -import { - BooleanField, - type BooleanFieldProps, - Icon, - Label, - Nav, -} from '../../atoms'; -import { NavList, NavItem, NavLink } from '../../molecules'; +import { BooleanField, type BooleanFieldProps, Icon, Label } from '../../atoms'; +import { type MainNavItem as Item, MainNav } from '../nav'; import mainNavStyles from './main-nav.module.scss'; import sharedStyles from './toolbar-items.module.scss'; -export type MainNavItem = { - id: string; - href: string; - label: string; - logo?: ReactNode; -}; - -export type MainNavProps = { +export type MainNavItemProps = { /** * Set additional classnames to the nav element. */ @@ -34,17 +17,17 @@ export type MainNavProps = { /** * The main nav items. */ - items: MainNavItem[]; + items: Item[]; /** * A callback function to handle button state. */ setIsActive: BooleanFieldProps['onChange']; }; -const MainNavWithRef: ForwardRefRenderFunction<HTMLDivElement, MainNavProps> = ( - { className = '', isActive = false, items, setIsActive }, - ref -) => { +const MainNavItemWithRef: ForwardRefRenderFunction< + HTMLDivElement, + MainNavItemProps +> = ({ className = '', isActive = false, items, setIsActive }, ref) => { const intl = useIntl(); const label = isActive ? intl.formatMessage({ @@ -76,24 +59,17 @@ const MainNavWithRef: ForwardRefRenderFunction<HTMLDivElement, MainNavProps> = ( > <Icon shape="hamburger" /> </Label> - <Nav + <MainNav className={`${sharedStyles.modal} ${mainNavStyles.modal} ${className}`} - > - <NavList isInline spacing="2xs"> - {items.map(({ id, ...link }) => ( - <NavItem key={id}> - <NavLink {...link} isStack variant="main" /> - </NavItem> - ))} - </NavList> - </Nav> + items={items} + /> </div> ); }; /** - * MainNav component + * MainNavItem component * - * Render the main navigation. + * Render the main navigation as toolbar item. */ -export const MainNav = forwardRef(MainNavWithRef); +export const MainNavItem = forwardRef(MainNavItemWithRef); diff --git a/src/components/organisms/toolbar/toolbar.tsx b/src/components/organisms/toolbar/toolbar.tsx index 94c9d95..999a29a 100644 --- a/src/components/organisms/toolbar/toolbar.tsx +++ b/src/components/organisms/toolbar/toolbar.tsx @@ -1,6 +1,7 @@ -import { FC, useState } from 'react'; +/* eslint-disable max-statements */ +import { type FC, useState, useCallback } from 'react'; import { useOnClickOutside, useRouteChange } from '../../../utils/hooks'; -import { MainNav, type MainNavProps } from './main-nav'; +import { MainNavItem, type MainNavItemProps } from './main-nav'; import { Search, type SearchProps } from './search'; import { Settings, type SettingsProps } from './settings'; import styles from './toolbar.module.scss'; @@ -14,7 +15,7 @@ export type ToolbarProps = Pick<SearchProps, 'searchPage'> & /** * The main nav items. */ - nav: MainNavProps['items']; + nav: MainNavItemProps['items']; }; /** @@ -43,23 +44,36 @@ export const Toolbar: FC<ToolbarProps> = ({ () => isSettingsOpened && setIsSettingsOpened(false) ); + const toggleMainNav = useCallback( + () => setIsNavOpened((prevState) => !prevState), + [] + ); + const toggleSearch = useCallback( + () => setIsSearchOpened((prevState) => !prevState), + [] + ); + const toggleSettings = useCallback( + () => setIsSettingsOpened((prevState) => !prevState), + [] + ); + useRouteChange(() => setIsSearchOpened(false)); return ( <div className={`${styles.wrapper} ${className}`}> - <MainNav + <MainNavItem className={styles.modal} isActive={isNavOpened} items={nav} ref={mainNavRef} - setIsActive={() => setIsNavOpened(!isNavOpened)} + setIsActive={toggleMainNav} /> <Search className={`${styles.modal} ${styles['modal--search']}`} isActive={isSearchOpened} ref={searchRef} searchPage={searchPage} - setIsActive={() => setIsSearchOpened(!isSearchOpened)} + setIsActive={toggleSearch} /> <Settings ackeeStorageKey={ackeeStorageKey} @@ -67,7 +81,7 @@ export const Toolbar: FC<ToolbarProps> = ({ isActive={isSettingsOpened} motionStorageKey={motionStorageKey} ref={settingsRef} - setIsActive={() => setIsSettingsOpened(!isSettingsOpened)} + setIsActive={toggleSettings} /> </div> ); |
