diff --git a/.changeset/tiny-pillows-kick.md b/.changeset/tiny-pillows-kick.md new file mode 100644 index 00000000..07b2782d --- /dev/null +++ b/.changeset/tiny-pillows-kick.md @@ -0,0 +1,5 @@ +--- +'@smile/react-front-kit': minor +--- + +Added `HeaderMobile` and `HeaderNav` components, reworked `Header` with mobile mode, added optional placeholder to `SearchBar`, improved info returned by collapseButtonProps in `SidebarMenu` diff --git a/packages/react-front-kit/src/Components/CollapseButton/CollapseButtonControlled.tsx b/packages/react-front-kit/src/Components/CollapseButton/CollapseButtonControlled.tsx index 212efed7..f95bb14b 100644 --- a/packages/react-front-kit/src/Components/CollapseButton/CollapseButtonControlled.tsx +++ b/packages/react-front-kit/src/Components/CollapseButton/CollapseButtonControlled.tsx @@ -185,7 +185,7 @@ export function CollapseButtonControlled< {...buttonProps} > - )} - {Boolean(opened) && ( - - - - )} - {right} - - - + {children} + + +
+ {Boolean(hasSearch) && ( + + )} + {Boolean(searchOpened) && ( + + + + )} +
+ {right} +
+ + + + {/* Mobile Header */} +
+ +
+ ); } diff --git a/packages/react-front-kit/src/Components/Header/__snapshots__/Header.test.tsx.snap b/packages/react-front-kit/src/Components/Header/__snapshots__/Header.test.tsx.snap index 7b12d0a2..0e979f0d 100644 --- a/packages/react-front-kit/src/Components/Header/__snapshots__/Header.test.tsx.snap +++ b/packages/react-front-kit/src/Components/Header/__snapshots__/Header.test.tsx.snap @@ -2,66 +2,170 @@ exports[`Header matches snapshot 1`] = `
-
- +
+
+
+
- Archives - + + +
+
- +
-
-
+
+
`; diff --git a/packages/react-front-kit/src/Components/HeaderMobile/HeaderMobile.stories.tsx b/packages/react-front-kit/src/Components/HeaderMobile/HeaderMobile.stories.tsx new file mode 100644 index 00000000..c7807cc4 --- /dev/null +++ b/packages/react-front-kit/src/Components/HeaderMobile/HeaderMobile.stories.tsx @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { useStorybookArgsConnect } from '@smile/react-front-kit-shared/storybook-utils'; +import { action } from '@storybook/addon-actions'; + +import { + childrenMock, + leftContentMock, + rightContentMobileMock, +} from '../Header/Header.mock'; + +import { HeaderMobile as Cmp } from './HeaderMobile'; + +const meta = { + component: Cmp, + decorators: [ + function Component(Story, ctx) { + const args = useStorybookArgsConnect(ctx.args, { + onSearchChange: 'searchValue', + }); + return ; + }, + ], + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], + title: '3-custom/Components/HeaderMobile', +} satisfies Meta; + +export default meta; +type IStory = StoryObj; + +export const HeaderMobile: IStory = { + args: { + children: childrenMock(true), + height: 90, + left: leftContentMock, + onSearchSubmit: action('search input submitted'), + right: rightContentMobileMock, + searchValue: '', + withBorder: false, + }, +}; diff --git a/packages/react-front-kit/src/Components/HeaderMobile/HeaderMobile.style.tsx b/packages/react-front-kit/src/Components/HeaderMobile/HeaderMobile.style.tsx new file mode 100644 index 00000000..7b72af73 --- /dev/null +++ b/packages/react-front-kit/src/Components/HeaderMobile/HeaderMobile.style.tsx @@ -0,0 +1,43 @@ +import { createStyles } from '@mantine/core'; + +export const useStyles = createStyles((theme, height: number) => ({ + around: { + alignItems: 'center', + gap: theme.spacing.xs, + }, + burgerMenu: { + backgroundColor: theme.white, + borderTop: '1px solid', + borderTopColor: theme.colors.gray[2], + height: `calc(100vh - ${height}px)`, + padding: '40px 24px', + }, + burgerSearch: { + '.mantine-TextInput-input': { + padding: '6px 68px 6px 24px', + }, + marginTop: 'auto', + }, + containerMobile: { + alignItems: 'center', + justifyContent: 'space-between', + padding: '16px 24px', + position: 'relative', + width: '100%', + }, + menu: { + alignItems: 'center', + gap: theme.spacing.xl, + left: '50%', + margin: 'auto', + position: 'absolute', + top: '50%', + translate: '-50% -50%', + }, + searchIcon: { + marginRight: '24px', + }, + separator: { + borderColor: theme.colors.gray[3], + }, +})); diff --git a/packages/react-front-kit/src/Components/HeaderMobile/HeaderMobile.test.tsx b/packages/react-front-kit/src/Components/HeaderMobile/HeaderMobile.test.tsx new file mode 100644 index 00000000..d00a9ddf --- /dev/null +++ b/packages/react-front-kit/src/Components/HeaderMobile/HeaderMobile.test.tsx @@ -0,0 +1,16 @@ +import { renderWithProviders } from '@smile/react-front-kit-shared/test-utils'; + +import { HeaderMobile } from './HeaderMobile'; + +describe('HeaderMobile', () => { + it('matches snapshot', () => { + const { container } = renderWithProviders( + + Espace documentaire + Espace workflow + Archives + , + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-front-kit/src/Components/HeaderMobile/HeaderMobile.tsx b/packages/react-front-kit/src/Components/HeaderMobile/HeaderMobile.tsx new file mode 100644 index 00000000..6a237285 --- /dev/null +++ b/packages/react-front-kit/src/Components/HeaderMobile/HeaderMobile.tsx @@ -0,0 +1,107 @@ +'use client'; + +import type { + BurgerProps, + CollapseProps, + HeaderProps, + TextInputProps, +} from '@mantine/core'; +import type { FormEvent, ReactElement, ReactNode } from 'react'; + +import { + Burger, + Collapse, + Divider, + Flex, + Header as MantineHeader, + Stack, + TextInput, + useMantineTheme, +} from '@mantine/core'; +import { MagnifyingGlass } from '@phosphor-icons/react'; +import { useState } from 'react'; + +import { useStyles } from './HeaderMobile.style'; + +export interface IHeaderMobileProps + extends Omit { + burgerProps?: Omit; + children?: ReactNode; + collapseProps?: Omit; + hasSearch?: boolean; + height?: number; + left?: ReactNode; + onSearchChange?: (value: string) => void; + onSearchSubmit?: (event: FormEvent) => void; + right?: ReactNode; + searchInputProps?: Omit; + searchValue?: string; +} + +/** Additional props will be forwarded to the [Mantine AppShell (Header) component](https://mantine.dev/core/app-shell/) */ +export function HeaderMobile(props: IHeaderMobileProps): ReactElement { + const { + burgerProps, + children, + collapseProps, + hasSearch = true, + height = 76, + left, + onSearchChange, + onSearchSubmit, + right, + searchInputProps, + searchValue, + withBorder = true, + ...headerProps + } = props; + const [burgerOpened, setBurgerOpened] = useState(false); + + const theme = useMantineTheme(); + const { classes } = useStyles(height); + + return ( + + + + setBurgerOpened((o) => !o)} + opened={burgerOpened} + size={22} + title="Open navigation" + {...burgerProps} + /> + + + {left} + {right} + + + + {children} + {Boolean(hasSearch) && ( +
+ onSearchChange?.(e.target.value)} + placeholder="Search on the site" + radius={32} + rightSection={ + + } + size="md" + value={searchValue} + {...searchInputProps} + /> + + )} +
+
+
+ ); +} diff --git a/packages/react-front-kit/src/Components/HeaderMobile/__snapshots__/HeaderMobile.test.tsx.snap b/packages/react-front-kit/src/Components/HeaderMobile/__snapshots__/HeaderMobile.test.tsx.snap new file mode 100644 index 00000000..282c86e0 --- /dev/null +++ b/packages/react-front-kit/src/Components/HeaderMobile/__snapshots__/HeaderMobile.test.tsx.snap @@ -0,0 +1,102 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HeaderMobile matches snapshot 1`] = ` +
+
+
+
+ + +
+
+
+ +
+
+`; diff --git a/packages/react-front-kit/src/Components/HeaderNav/HeaderNav.stories.tsx b/packages/react-front-kit/src/Components/HeaderNav/HeaderNav.stories.tsx new file mode 100644 index 00000000..c4a77cee --- /dev/null +++ b/packages/react-front-kit/src/Components/HeaderNav/HeaderNav.stories.tsx @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Flex } from '@mantine/core'; + +import { menusMock } from '../Header/Header.mock'; + +import { HeaderNav as Cmp } from './HeaderNav'; + +const meta = { + component: Cmp, + tags: ['autodocs'], + title: '3-custom/Components/HeaderNav', +} satisfies Meta; + +export default meta; +type IStory = StoryObj; + +export const HeaderNav: IStory = { + args: { + menus: menusMock, + }, + render: ({ ...props }) => ( + + + + ), +}; + +export const Mobile: IStory = { + args: { + isMobile: true, + menus: menusMock, + }, +}; diff --git a/packages/react-front-kit/src/Components/HeaderNav/HeaderNav.style.tsx b/packages/react-front-kit/src/Components/HeaderNav/HeaderNav.style.tsx new file mode 100644 index 00000000..2660f338 --- /dev/null +++ b/packages/react-front-kit/src/Components/HeaderNav/HeaderNav.style.tsx @@ -0,0 +1,47 @@ +import { createStyles } from '@mantine/core'; + +export const useStyles = createStyles((theme) => ({ + activeLabel: { + '& span': { + color: theme.colors.dark[6], + }, + }, + activeRootLabel: { + '& span': { + color: theme.fn.primaryColor(), + }, + }, + dropdown: { + borderRadius: 10, + padding: '16px 20px', + }, + menu: { + ':hover': { backgroundColor: theme.colors.gray[1] }, + backgroundColor: 'transparent', + }, + navLink: { + borderRadius: 10, + width: 'unset', + }, + navLinkLabel: { + fontSize: '14px', + fontWeight: 400, + margin: 0, + padding: 0, + }, + navLinkParentLabel: { + fontSize: '14px', + fontWeight: 600, + margin: 0, + padding: 0, + }, + rootMenu: { + '& span': { + color: theme.colors.dark[6], + }, + ':not(:first-of-type)': { + marginTop: '32px', + }, + fontSize: '18px', + }, +})); diff --git a/packages/react-front-kit/src/Components/HeaderNav/HeaderNav.test.tsx b/packages/react-front-kit/src/Components/HeaderNav/HeaderNav.test.tsx new file mode 100644 index 00000000..77d2dd92 --- /dev/null +++ b/packages/react-front-kit/src/Components/HeaderNav/HeaderNav.test.tsx @@ -0,0 +1,22 @@ +import { renderWithProviders } from '@smile/react-front-kit-shared/test-utils'; + +import { menusMock } from '../Header/Header.mock'; + +import { HeaderNav } from './HeaderNav'; + +describe('HeaderNav', () => { + beforeEach(() => { + // Prevent mantine random ID + Math.random = () => 0.42; + }); + it('matches snapshot in desktop mode', () => { + const { container } = renderWithProviders(); + expect(container).toMatchSnapshot(); + }); + it('matches snapshot in mobile mode', () => { + const { container } = renderWithProviders( + , + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-front-kit/src/Components/HeaderNav/HeaderNav.tsx b/packages/react-front-kit/src/Components/HeaderNav/HeaderNav.tsx new file mode 100644 index 00000000..620afbe8 --- /dev/null +++ b/packages/react-front-kit/src/Components/HeaderNav/HeaderNav.tsx @@ -0,0 +1,122 @@ +'use client'; + +import type { IMenuItem } from '../SidebarMenu/SidebarMenu'; +import type { ElementType, ReactElement, ReactNode } from 'react'; + +import { Menu, NavLink, useMantineTheme } from '@mantine/core'; +import { CaretDown } from '@phosphor-icons/react'; + +import { SidebarMenu } from '../SidebarMenu/SidebarMenu'; + +import { useStyles } from './HeaderNav.style'; + +function recursiveNavLinks( + menus: IHeaderNavMenu[], + component: ElementType, + className: string, +): ReactNode { + return menus.map((menu) => ( + + {menu.children + ? recursiveNavLinks(menu.children, component, className) + : null} + + )); +} + +export interface IHeaderNavMenu + extends IMenuItem { + children?: IHeaderNavMenu[]; + url?: string; +} + +export interface IHeaderNavProps { + isMobile?: boolean; + menus: IHeaderNavMenu[]; + navLinkComponent?: ElementType; +} + +export function HeaderNav( + props: IHeaderNavProps, +): ReactElement { + const { menus, isMobile = false, navLinkComponent = 'a' } = props; + const theme = useMantineTheme(); + const { classes } = useStyles(); + + return ( + <> + {isMobile ? ( + , + level: number, + isSelected: boolean, + opened: boolean, + ) => { + return { + className: [ + classes.menu, + level === 0 ? classes.rootMenu : '', + opened || isSelected + ? level === 0 + ? classes.activeRootLabel + : classes.activeLabel + : '', + ].join(' '), + ...(level >= 1 && { + collapseProps: { + className: '', + }, + }), + }; + }} + menu={menus} + p={0} + /> + ) : ( + menus.map((menu) => { + if (menu.children) { + return ( + + + + } + /> + + + {recursiveNavLinks( + menu.children, + navLinkComponent, + classes.navLink, + )} + + + ); + } + return ( + + ); + }) + )} + + ); +} diff --git a/packages/react-front-kit/src/Components/HeaderNav/__snapshots__/HeaderNav.test.tsx.snap b/packages/react-front-kit/src/Components/HeaderNav/__snapshots__/HeaderNav.test.tsx.snap new file mode 100644 index 00000000..8901705a --- /dev/null +++ b/packages/react-front-kit/src/Components/HeaderNav/__snapshots__/HeaderNav.test.tsx.snap @@ -0,0 +1,404 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HeaderNav matches snapshot in desktop mode 1`] = ` +
+ +