aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-10-25 17:23:53 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:15:27 +0100
commitc21a137e1991af1331fe5768fc6bac15ea9230b1 (patch)
tree80569408dbed888273a15d9ae543f553f2798a9b /src/components/organisms
parent73e12fe8ae059ef70bbdf8716af421cb72aec76c (diff)
refactor(components): extract MainNav component from toolbar
Diffstat (limited to 'src/components/organisms')
-rw-r--r--src/components/organisms/nav/index.ts1
-rw-r--r--src/components/organisms/nav/main-nav/index.ts1
-rw-r--r--src/components/organisms/nav/main-nav/main-nav.module.scss40
-rw-r--r--src/components/organisms/nav/main-nav/main-nav.stories.tsx71
-rw-r--r--src/components/organisms/nav/main-nav/main-nav.test.tsx18
-rw-r--r--src/components/organisms/nav/main-nav/main-nav.tsx47
-rw-r--r--src/components/organisms/toolbar/index.ts3
-rw-r--r--src/components/organisms/toolbar/main-nav.stories.tsx34
-rw-r--r--src/components/organisms/toolbar/main-nav.test.tsx37
-rw-r--r--src/components/organisms/toolbar/main-nav.tsx54
-rw-r--r--src/components/organisms/toolbar/toolbar.tsx28
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>
);