diff options
Diffstat (limited to 'src')
21 files changed, 264 insertions, 278 deletions
| diff --git a/src/components/atoms/lists/list/list-item.tsx b/src/components/atoms/lists/list/list-item.tsx index 3438eac..41e90fc 100644 --- a/src/components/atoms/lists/list/list-item.tsx +++ b/src/components/atoms/lists/list/list-item.tsx @@ -1,23 +1,39 @@ -import type { FC, LiHTMLAttributes } from 'react'; +import { +  forwardRef, +  type LiHTMLAttributes, +  type ForwardRefRenderFunction, +} from 'react';  import styles from './list.module.scss'; -export type ListItemProps = LiHTMLAttributes<HTMLElement>; +export type ListItemProps = LiHTMLAttributes<HTMLElement> & { +  /** +   * Should we hide the marker in front of the list item? +   * +   * @default false +   */ +  hideMarker?: boolean; +}; -/** - * ListItem component - * - * Used it inside a `<List />` component. - */ -export const ListItem: FC<ListItemProps> = ({ -  children, -  className = '', -  ...props -}) => { -  const itemClass = `${styles.item} ${className}`; +const ListItemWithRef: ForwardRefRenderFunction< +  HTMLLIElement, +  ListItemProps +> = ({ children, className = '', hideMarker = false, ...props }, ref) => { +  const itemClass = [ +    styles.item, +    styles[hideMarker ? 'item--no-marker' : ''], +    className, +  ].join(' ');    return ( -    <li {...props} className={itemClass}> +    <li {...props} className={itemClass} ref={ref}>        {children}      </li>    );  }; + +/** + * ListItem component + * + * Used it inside a `<List />` component. + */ +export const ListItem = forwardRef(ListItemWithRef); diff --git a/src/components/atoms/lists/list/list.module.scss b/src/components/atoms/lists/list/list.module.scss index fc23ce5..d9d1f64 100644 --- a/src/components/atoms/lists/list/list.module.scss +++ b/src/components/atoms/lists/list/list.module.scss @@ -1,5 +1,11 @@  @use "../../../../styles/abstracts/placeholders"; +.item { +  &--no-marker { +    list-style-type: none; +  } +} +  .list {    &--hierarchical {      @extend %hierarchical-list; diff --git a/src/components/molecules/nav/index.ts b/src/components/molecules/nav/index.ts index fe7cd0b..ca84088 100644 --- a/src/components/molecules/nav/index.ts +++ b/src/components/molecules/nav/index.ts @@ -1,4 +1,5 @@  export * from './breadcrumb'; +export * from './nav-item';  export * from './nav-link';  export * from './nav-list';  export * from './pagination'; diff --git a/src/components/molecules/nav/nav-item/index.ts b/src/components/molecules/nav/nav-item/index.ts new file mode 100644 index 0000000..d6f1411 --- /dev/null +++ b/src/components/molecules/nav/nav-item/index.ts @@ -0,0 +1 @@ +export * from './nav-item'; diff --git a/src/components/molecules/nav/nav-item/nav-item.stories.tsx b/src/components/molecules/nav/nav-item/nav-item.stories.tsx new file mode 100644 index 0000000..df736a4 --- /dev/null +++ b/src/components/molecules/nav/nav-item/nav-item.stories.tsx @@ -0,0 +1,37 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { NavLink } from '../nav-link'; +import { NavItem } from './nav-item'; + +/** + * NavItem - Storybook Meta + */ +export default { +  title: 'Molecules/Nav/NavItem', +  component: NavItem, +  argTypes: { +    children: { +      control: { +        type: 'text', +      }, +      description: 'Define the nav item contents.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +  }, +} as ComponentMeta<typeof NavItem>; + +const Template: ComponentStory<typeof NavItem> = (args) => ( +  <ul style={{ margin: 0, padding: 0 }}> +    <NavItem {...args} /> +  </ul> +); + +/** + * NavItem Stories - Default + */ +export const Default = Template.bind({}); +Default.args = { +  children: <NavLink href="#example" label="Example" />, +}; diff --git a/src/components/molecules/nav/nav-item/nav-item.test.tsx b/src/components/molecules/nav/nav-item/nav-item.test.tsx new file mode 100644 index 0000000..a78b97b --- /dev/null +++ b/src/components/molecules/nav/nav-item/nav-item.test.tsx @@ -0,0 +1,25 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { NavLink } from '../nav-link'; +import { NavItem } from './nav-item'; + +describe('NavItem', () => { +  it('renders its children', () => { +    const label = 'maxime'; +    const target = '#sunt'; + +    render( +      <ul> +        <NavItem> +          <NavLink href={target} label={label} /> +        </NavItem> +      </ul> +    ); + +    expect(rtlScreen.getByRole('listitem')).toHaveTextContent(label); +    expect(rtlScreen.getByRole('link', { name: label })).toHaveAttribute( +      'href', +      target +    ); +  }); +}); diff --git a/src/components/molecules/nav/nav-item/nav-item.tsx b/src/components/molecules/nav/nav-item/nav-item.tsx new file mode 100644 index 0000000..2e85043 --- /dev/null +++ b/src/components/molecules/nav/nav-item/nav-item.tsx @@ -0,0 +1,24 @@ +import { +  type ForwardRefRenderFunction, +  forwardRef, +  type ReactNode, +} from 'react'; +import { ListItem, type ListItemProps } from '../../../atoms'; + +export type NavItemProps = Omit<ListItemProps, 'children' | 'hideMarker'> & { +  /** +   * The nav item contents. +   */ +  children: ReactNode; +}; + +const NavItemWithRef: ForwardRefRenderFunction<HTMLLIElement, NavItemProps> = ( +  { children, ...props }, +  ref +) => ( +  <ListItem {...props} hideMarker ref={ref}> +    {children} +  </ListItem> +); + +export const NavItem = forwardRef(NavItemWithRef); diff --git a/src/components/molecules/nav/nav-list.module.scss b/src/components/molecules/nav/nav-list.module.scss deleted file mode 100644 index ff99581..0000000 --- a/src/components/molecules/nav/nav-list.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -.nav { -  &--footer & { -    &__item:not(:first-child) { -      &::before { -        content: "\2022"; -        margin-right: var(--spacing-2xs); -      } -    } -  } -} diff --git a/src/components/molecules/nav/nav-list.stories.tsx b/src/components/molecules/nav/nav-list.stories.tsx deleted file mode 100644 index baaa8df..0000000 --- a/src/components/molecules/nav/nav-list.stories.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Icon } from '../../atoms'; -import { NavList, type NavItem } from './nav-list'; - -/** - * Nav - Storybook Meta - */ -export default { -  title: 'Molecules/Navigation/Nav', -  component: NavList, -  argTypes: { -    'aria-label': { -      control: { -        type: 'text', -      }, -      description: 'An accessible name for the navigation.', -      table: { -        category: 'Accessibility', -      }, -      type: { -        name: 'string', -        required: false, -      }, -    }, -    className: { -      control: { -        type: 'text', -      }, -      description: 'Set additional classnames to the navigation wrapper.', -      table: { -        category: 'Styles', -      }, -      type: { -        name: 'string', -        required: false, -      }, -    }, -    items: { -      control: { -        type: null, -      }, -      description: 'The nav items.', -      type: { -        name: 'other', -        required: true, -        value: '', -      }, -    }, -    kind: { -      control: { -        type: 'select', -      }, -      description: 'The navigation kind.', -      options: ['main', 'footer'], -      type: { -        name: 'string', -        required: true, -      }, -    }, -    listClassName: { -      control: { -        type: 'text', -      }, -      description: 'Set additional classnames to the navigation list.', -      table: { -        category: 'Styles', -      }, -      type: { -        name: 'string', -        required: false, -      }, -    }, -  }, -} as ComponentMeta<typeof NavList>; - -const Template: ComponentStory<typeof NavList> = (args) => ( -  <NavList {...args} /> -); - -const MainNavItems: NavItem[] = [ -  { id: 'homeLink', href: '/', label: 'Home', logo: <Icon shape="home" /> }, -  { -    id: 'contactLink', -    href: '/contact', -    label: 'Contact', -    logo: <Icon shape="envelop" />, -  }, -]; - -const FooterNavItems: NavItem[] = [ -  { id: 'contactLink', href: '/contact', label: 'Contact' }, -  { id: 'legalLink', href: '/legal-notice', label: 'Legal notice' }, -]; - -/** - * Nav Stories - Main navigation - */ -export const MainNav = Template.bind({}); -MainNav.args = { -  items: MainNavItems, -  kind: 'main', -}; - -/** - * Nav Stories - Footer navigation - */ -export const FooterNav = Template.bind({}); -FooterNav.args = { -  items: FooterNavItems, -  kind: 'footer', -}; diff --git a/src/components/molecules/nav/nav-list.test.tsx b/src/components/molecules/nav/nav-list.test.tsx deleted file mode 100644 index 8524e22..0000000 --- a/src/components/molecules/nav/nav-list.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../tests/utils'; -import { Icon } from '../../atoms'; -import { NavList, type NavItem } from './nav-list'; - -const navItems: NavItem[] = [ -  { id: 'homeLink', href: '/', label: 'Home', logo: <Icon shape="home" /> }, -  { -    id: 'contactLink', -    href: '/contact', -    label: 'Contact', -    logo: <Icon shape="envelop" />, -  }, -]; - -describe('Nav', () => { -  it('renders a main navigation', () => { -    render(<NavList kind="main" items={navItems} />); -    expect(rtlScreen.getByRole('navigation')).toHaveClass('nav--main'); -  }); - -  it('renders a footer navigation', () => { -    render(<NavList kind="footer" items={navItems} />); -    expect(rtlScreen.getByRole('navigation')).toHaveClass('nav--footer'); -  }); - -  it('renders navigation links', () => { -    render(<NavList kind="main" items={navItems} />); -    expect( -      rtlScreen.getByRole('link', { name: navItems[0].label }) -    ).toHaveAttribute('href', navItems[0].href); -  }); -}); diff --git a/src/components/molecules/nav/nav-list.tsx b/src/components/molecules/nav/nav-list.tsx deleted file mode 100644 index a6acdcf..0000000 --- a/src/components/molecules/nav/nav-list.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import type { FC, ReactNode } from 'react'; -import { Link, List, ListItem, Nav, type NavProps } from '../../atoms'; -import { NavLink } from './nav-link'; -import styles from './nav-list.module.scss'; - -export type NavItem = { -  /** -   * The item id. -   */ -  id: string; -  /** -   * The item link. -   */ -  href: string; -  /** -   * The item name. -   */ -  label: string; -  /** -   * The item logo. -   */ -  logo?: ReactNode; -}; - -export type NavListProps = Omit<NavProps, 'children'> & { -  /** -   * The navigation items. -   */ -  items: NavItem[]; -  /** -   * The navigation kind. -   */ -  kind: 'main' | 'footer'; -  /** -   * Set additional classnames to the navigation list. -   */ -  listClassName?: string; -}; - -/** - * Nav component - * - * Render the nav links. - */ -export const NavList: FC<NavListProps> = ({ -  className = '', -  items, -  kind, -  listClassName = '', -  ...props -}) => { -  const navClass = [styles[`nav--${kind}`], className].join(' '); - -  /** -   * Get the nav items. -   * @returns {JSX.Element[]} An array of nav items. -   */ -  const getItems = (): JSX.Element[] => -    items.map(({ id, href, label, logo }) => ( -      <ListItem key={id} className={styles.nav__item}> -        {kind === 'main' ? ( -          <NavLink href={href} label={label} logo={logo} variant="main" /> -        ) : ( -          <Link href={href}>{label}</Link> -        )} -      </ListItem> -    )); - -  return ( -    <Nav {...props} className={navClass}> -      <List className={listClassName} hideMarker isInline spacing="2xs"> -        {getItems()} -      </List> -    </Nav> -  ); -}; diff --git a/src/components/molecules/nav/nav-list/index.ts b/src/components/molecules/nav/nav-list/index.ts new file mode 100644 index 0000000..8f253d2 --- /dev/null +++ b/src/components/molecules/nav/nav-list/index.ts @@ -0,0 +1 @@ +export * from './nav-list'; diff --git a/src/components/molecules/nav/nav-list/nav-list.stories.tsx b/src/components/molecules/nav/nav-list/nav-list.stories.tsx new file mode 100644 index 0000000..c165ac7 --- /dev/null +++ b/src/components/molecules/nav/nav-list/nav-list.stories.tsx @@ -0,0 +1,51 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { NavItem } from '../nav-item'; +import { NavLink } from '../nav-link'; +import { NavList } from './nav-list'; + +/** + * Nav - Storybook Meta + */ +export default { +  title: 'Molecules/Nav/NavList', +  component: NavList, +} as ComponentMeta<typeof NavList>; + +const Template: ComponentStory<typeof NavList> = (args) => ( +  <NavList {...args} /> +); + +const NavItems = () => ( +  <> +    <NavItem> +      <NavLink href="#item1" label="Item 1" /> +    </NavItem> +    <NavItem> +      <NavLink href="#item2" label="Item 2" /> +    </NavItem> +    <NavItem> +      <NavLink href="#item3" label="Item 3" /> +    </NavItem> +    <NavItem> +      <NavLink href="#item4" label="Item 4" /> +    </NavItem> +  </> +); + +/** + * NavList Stories - Default + */ +export const Default = Template.bind({}); +Default.args = { +  children: <NavItems />, +}; + +/** + * NavList Stories - Inlined + */ +export const Inlined = Template.bind({}); +Inlined.args = { +  children: <NavItems />, +  isInline: true, +  spacing: 'sm', +}; diff --git a/src/components/molecules/nav/nav-list/nav-list.test.tsx b/src/components/molecules/nav/nav-list/nav-list.test.tsx new file mode 100644 index 0000000..ac08681 --- /dev/null +++ b/src/components/molecules/nav/nav-list/nav-list.test.tsx @@ -0,0 +1,15 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { NavList } from './nav-list'; + +describe('NavList', () => { +  it('renders its children', () => { +    render( +      <NavList> +        <li>Nav item</li> +      </NavList> +    ); + +    expect(rtlScreen.getByRole('list')).toBeInTheDocument(); +  }); +}); diff --git a/src/components/molecules/nav/nav-list/nav-list.tsx b/src/components/molecules/nav/nav-list/nav-list.tsx new file mode 100644 index 0000000..bdf7487 --- /dev/null +++ b/src/components/molecules/nav/nav-list/nav-list.tsx @@ -0,0 +1,23 @@ +import { forwardRef, type ReactNode, type ForwardedRef } from 'react'; +import { List, type ListProps } from '../../../atoms'; + +export type NavListProps<T extends boolean> = Omit< +  ListProps<T, false>, +  'children' | 'hideMarker' +> & { +  /** +   * The nav items. +   */ +  children: ReactNode; +}; + +const NavListWithRef = <T extends boolean>( +  { children, isInline, ...props }: NavListProps<T>, +  ref: ForwardedRef<HTMLUListElement | HTMLOListElement> +) => ( +  <List {...props} hideMarker isInline={isInline} ref={ref}> +    {children} +  </List> +); + +export const NavList = forwardRef(NavListWithRef); diff --git a/src/components/organisms/layout/site-footer.module.scss b/src/components/organisms/layout/site-footer.module.scss index a8bcd61..a502763 100644 --- a/src/components/organisms/layout/site-footer.module.scss +++ b/src/components/organisms/layout/site-footer.module.scss @@ -19,9 +19,6 @@  }  .nav { -  display: flex; -  flex-flow: row wrap; -    @include mix.media("screen") {      @include mix.dimensions("sm") {        &::before { diff --git a/src/components/organisms/layout/site-footer.tsx b/src/components/organisms/layout/site-footer.tsx index 0866924..b4930d6 100644 --- a/src/components/organisms/layout/site-footer.tsx +++ b/src/components/organisms/layout/site-footer.tsx @@ -5,10 +5,17 @@ import {    BackToTop,    type BackToTopProps,    NavList, -  type NavItem, +  NavItem, +  NavLink,  } from '../../molecules';  import styles from './site-footer.module.scss'; +export type FooterLinks = { +  id: string; +  href: string; +  label: string; +}; +  export type SiteFooterProps = {    /**     * Set additional classnames to the back to top button. @@ -25,7 +32,7 @@ export type SiteFooterProps = {    /**     * The footer nav items.     */ -  navItems?: NavItem[]; +  navItems?: FooterLinks[];    /**     * An element id (without hashtag) used as anchor for back to top button.     */ @@ -67,13 +74,13 @@ export const SiteFooter: FC<SiteFooterProps> = ({          owner={copyright.owner}        />        {navItems ? ( -        <NavList -          aria-label={ariaLabel} -          className={styles.nav} -          items={navItems} -          // eslint-disable-next-line react/jsx-no-literals -- Kind allowed -          kind="footer" -        /> +        <NavList aria-label={ariaLabel} className={styles.nav} isInline> +          {navItems.map(({ id, ...link }) => ( +            <NavItem key={id}> +              <NavLink {...link} /> +            </NavItem> +          ))} +        </NavList>        ) : null}        <BackToTop          anchor={backToTopAnchor} diff --git a/src/components/organisms/toolbar/main-nav.module.scss b/src/components/organisms/toolbar/main-nav.module.scss index 1b6b110..bedf38e 100644 --- a/src/components/organisms/toolbar/main-nav.module.scss +++ b/src/components/organisms/toolbar/main-nav.module.scss @@ -43,16 +43,6 @@      }    } -  .modal__list { -    display: flex; - -    @include mix.media("screen") { -      @include mix.dimensions("sm", "md") { -        flex-flow: column; -      } -    } -  } -    .checkbox {      &:checked {        ~ .label .icon { diff --git a/src/components/organisms/toolbar/main-nav.tsx b/src/components/organisms/toolbar/main-nav.tsx index 7f03577..a5a105e 100644 --- a/src/components/organisms/toolbar/main-nav.tsx +++ b/src/components/organisms/toolbar/main-nav.tsx @@ -1,15 +1,32 @@ -import { forwardRef, type ForwardRefRenderFunction } from 'react'; +import { +  forwardRef, +  type ReactNode, +  type ForwardRefRenderFunction, +} from 'react';  import { useIntl } from 'react-intl'; -import { BooleanField, type BooleanFieldProps, Icon, Label } from '../../atoms'; -import { NavList, type NavListProps, type NavItem } from '../../molecules'; +import { +  BooleanField, +  type BooleanFieldProps, +  Icon, +  Label, +  Nav, +} from '../../atoms'; +import { NavList, NavItem, NavLink } from '../../molecules';  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 = {    /**     * Set additional classnames to the nav element.     */ -  className?: NavListProps['className']; +  className?: string;    /**     * The button state.     */ @@ -17,7 +34,7 @@ export type MainNavProps = {    /**     * The main nav items.     */ -  items: NavItem[]; +  items: MainNavItem[];    /**     * A callback function to handle button state.     */ @@ -59,12 +76,17 @@ const MainNavWithRef: ForwardRefRenderFunction<HTMLDivElement, MainNavProps> = (        >          <Icon shape="hamburger" />        </Label> -      <NavList +      <Nav          className={`${sharedStyles.modal} ${mainNavStyles.modal} ${className}`} -        items={items} -        kind="main" -        listClassName={mainNavStyles.modal__list} -      /> +      > +        <NavList isInline spacing="2xs"> +          {items.map(({ id, ...link }) => ( +            <NavItem key={id}> +              <NavLink {...link} isStack variant="main" /> +            </NavItem> +          ))} +        </NavList> +      </Nav>      </div>    );  }; diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx index d9f7031..c2d3588 100644 --- a/src/pages/cv.tsx +++ b/src/pages/cv.tsx @@ -124,7 +124,7 @@ const components: MDXComponents = {    h4: H4,    h5: H5,    h6: H6, -  li: ListItem, +  li: ({ ref, ...props }) => <ListItem {...props} />,    Link,    ol: OrderedList,    ul: UnorderedList, diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx index 17be961..d397d27 100644 --- a/src/pages/projets/[slug].tsx +++ b/src/pages/projets/[slug].tsx @@ -140,7 +140,7 @@ const components: MDXComponents = {    h5: H5,    h6: H6,    Image: BorderedImage, -  li: ListItem, +  li: ({ ref, ...props }) => <ListItem {...props} />,    Link,    ol: OrderedList,    ul: UnorderedList, | 
