From c21a137e1991af1331fe5768fc6bac15ea9230b1 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Wed, 25 Oct 2023 17:23:53 +0200 Subject: refactor(components): extract MainNav component from toolbar --- src/components/organisms/nav/index.ts | 1 + src/components/organisms/nav/main-nav/index.ts | 1 + .../organisms/nav/main-nav/main-nav.module.scss | 40 ++++++++++++ .../organisms/nav/main-nav/main-nav.stories.tsx | 71 ++++++++++++++++++++++ .../organisms/nav/main-nav/main-nav.test.tsx | 18 ++++++ src/components/organisms/nav/main-nav/main-nav.tsx | 47 ++++++++++++++ src/components/organisms/toolbar/index.ts | 3 - .../organisms/toolbar/main-nav.stories.tsx | 34 +++++------ src/components/organisms/toolbar/main-nav.test.tsx | 37 +++++++---- src/components/organisms/toolbar/main-nav.tsx | 54 +++++----------- src/components/organisms/toolbar/toolbar.tsx | 28 ++++++--- 11 files changed, 253 insertions(+), 81 deletions(-) create mode 100644 src/components/organisms/nav/main-nav/index.ts create mode 100644 src/components/organisms/nav/main-nav/main-nav.module.scss create mode 100644 src/components/organisms/nav/main-nav/main-nav.stories.tsx create mode 100644 src/components/organisms/nav/main-nav/main-nav.test.tsx create mode 100644 src/components/organisms/nav/main-nav/main-nav.tsx (limited to 'src') 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; + +const Template: ComponentStory = (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: , + }, + { + id: 'blog', + label: 'Blog', + href: '#blog', + logo: , + }, + { + id: 'projects', + label: 'Projects', + href: '#projects', + logo: , + }, + { + id: 'contact', + label: 'Contact', + href: '#contact', + logo: , + }, + ], +}; 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(); + + 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 & { + /** + * The main nav items. + */ + items: MainNavItem[]; +}; + +const MainNavWithRef: ForwardRefRenderFunction = ( + { className = '', items, ...props }, + ref +) => { + const wrapperClass = `${className}`; + + return ( + + ); +}; + +/** + * 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; +} as ComponentMeta; -const Template: ComponentStory = ({ +const Template: ComponentStory = ({ isActive = false, setIsActive: _setIsActive, ...args }) => { const [isOpen, setIsOpen] = useState(isActive); - return ( - { - setIsOpen(!isOpen); - }} - {...args} - /> - ); + const toggle = useCallback(() => { + setIsOpen((prevState) => !prevState); + }, []); + + return ; }; /** - * 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( null} />); - expect(screen.getByRole('checkbox')).toHaveAccessibleName('Open menu'); + render( + + ); + expect(rtlScreen.getByRole('checkbox')).toHaveAccessibleName('Open menu'); }); it('renders a checkbox to close main nav', () => { - render( null} />); - expect(screen.getByRole('checkbox')).toHaveAccessibleName('Close menu'); + render( + + ); + expect(rtlScreen.getByRole('checkbox')).toHaveAccessibleName('Close menu'); }); it('renders the correct number of items', () => { - render( null} />); - expect(screen.getAllByRole('listitem')).toHaveLength(items.length); + render( + + ); + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(items.length); }); it('renders some links with the right label', () => { - render( null} />); - expect(screen.getByRole('link', { name: items[0].label })).toHaveAttribute( - 'href', - items[0].href + render( + ); + 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 = ( - { 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 = ( > - + items={items} + /> ); }; /** - * 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 & /** * The main nav items. */ - nav: MainNavProps['items']; + nav: MainNavItemProps['items']; }; /** @@ -43,23 +44,36 @@ export const Toolbar: FC = ({ () => 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 (
- setIsNavOpened(!isNavOpened)} + setIsActive={toggleMainNav} /> setIsSearchOpened(!isSearchOpened)} + setIsActive={toggleSearch} /> = ({ isActive={isSettingsOpened} motionStorageKey={motionStorageKey} ref={settingsRef} - setIsActive={() => setIsSettingsOpened(!isSettingsOpened)} + setIsActive={toggleSettings} />
); -- cgit v1.2.3