📱(frontend) header small mobile friendly

We adapt the header to be small mobile friendly.
We added a burger menu to display the dropdown
menu on small mobile.
This commit is contained in:
Anthony LC
2024-10-04 13:24:57 +02:00
committed by Anthony LC
parent 399cf893ad
commit fe391523c8
14 changed files with 226 additions and 52 deletions

View File

@@ -10,8 +10,6 @@ test.describe('Header', () => {
test('checks all the elements are visible', async ({ page }) => {
const header = page.locator('header').first();
await expect(header.getByAltText('Gouvernement Logo')).toBeVisible();
await expect(header.getByAltText('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
'color',
@@ -67,6 +65,42 @@ test.describe('Header', () => {
});
});
test.describe('Header mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('it checks the header when mobile', async ({ page }) => {
const header = page.locator('header').first();
await expect(
header.getByRole('button', {
name: 'Les services de La Suite numérique',
}),
).toBeVisible();
await expect(
page.getByRole('button', {
name: 'Logout',
}),
).toBeHidden();
await expect(page.getByAltText('Language Icon')).toBeHidden();
await header.getByLabel('Open the header menu').click();
await expect(
page.getByRole('button', {
name: 'Logout',
}),
).toBeVisible();
await expect(page.getByAltText('Language Icon')).toBeVisible();
});
});
test.describe('Header: Log out', () => {
test.use({ storageState: { cookies: [], origins: [] } });

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useAuthStore } from '@/core/auth';
export const AccountDropdown = () => {
export const ButtonLogin = () => {
const { t } = useTranslation();
const { logout, authenticated, login } = useAuthStore();

View File

@@ -1,4 +1,4 @@
export * from './AccountDropdown';
export * from './api/types';
export * from './Auth';
export * from './ButtonLogin';
export * from './useAuthStore';

View File

@@ -0,0 +1,32 @@
.burgerIcon {
cursor: pointer;
transform: translate(-20%, 0%);
}
.burgerIcon path {
stroke-width: 40;
stroke-linecap: round;
fill: none;
transition: all 0.5s ease-in-out;
}
/* In menu form */
.burgerIcon path:first-child,
.burgerIcon path:last-child {
stroke-dasharray: 240px 910px;
}
.burgerIcon .middle_bar {
stroke-dasharray: 240px 240px;
}
/* In cross form */
.open path:first-child,
.open path:last-child {
stroke-dashoffset: -650px;
}
.open :nth-child(2) {
stroke-dasharray: 0 220px;
stroke-dashoffset: -120px;
}

View File

@@ -0,0 +1,18 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Burger } from './Burger';
describe('<Burger />', () => {
test('Burger interactions', () => {
const { rerender } = render(<Burger isOpen={true} />);
const burger = screen.getByRole('img');
expect(burger).toBeInTheDocument();
expect(burger.classList.contains('open')).toBeTruthy();
rerender(<Burger isOpen={false} />);
expect(burger.classList.contains('open')).not.toBeTruthy();
});
});

View File

@@ -0,0 +1,31 @@
import { SVGProps } from 'react';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import styles from './Burger.module.css';
import BurgerIcon from './burger.svg';
type BurgerProps = SVGProps<SVGSVGElement> & {
isOpen: boolean;
};
export const Burger = ({ isOpen, ...props }: BurgerProps) => {
const { colorsTokens } = useCunninghamTheme();
return (
<Box
$color={colorsTokens()['primary-text']}
$padding="none"
$justify="center"
>
<BurgerIcon
role="img"
className={`${styles.burgerIcon} ${isOpen ? styles.open : ''}`}
{...props}
/>
</Box>
);
};
export default Burger;

View File

@@ -0,0 +1,10 @@
<svg viewBox="280 230 400 200" stroke="currentColor">
<path
d="M300,220 C300,220 520,220 540,220 C740,220 640,540 520,420 C440,340 300,200 300,200"
/>
<path d="M300,320 L540,320" />
<path
d="M300,210 C300,210 520,210 540,210 C740,210 640,530 520,410 C440,330 300,190 300,190"
transform="translate(480, 320) scale(1, -1) translate(-480, -318)"
/>
</svg>

After

Width:  |  Height:  |  Size: 375 B

View File

@@ -0,0 +1,46 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, DropButton } from '@/components';
import { ButtonLogin } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { LanguagePicker } from '@/features/language';
import { Burger } from './Burger/Burger';
export const DropdownMenu = () => {
const { colorsTokens } = useCunninghamTheme();
const [isDropOpen, setIsDropOpen] = useState(false);
const { t } = useTranslation();
return (
<DropButton
button={
<Burger
isOpen={isDropOpen}
width={30}
height={30}
aria-controls="menu"
aria-label={t('Open the header menu')}
/>
}
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
isOpen={isDropOpen}
>
<Box $align="center" $direction="column">
<Box
$width="100%"
$align="center"
$height="36px"
$justify="center"
$css={`&:hover{background:${colorsTokens()['primary-150']}}`}
$hasTransition
$radius="2px"
>
<LanguagePicker />
</Box>
<ButtonLogin />
</Box>
</DropButton>
);
};

View File

@@ -1,59 +1,40 @@
import Image from 'next/image';
import React from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { Box, StyledLink, Text } from '@/components/';
import { AccountDropdown } from '@/core/auth';
import { useCunninghamTheme } from '@/cunningham';
import { ButtonLogin } from '@/core/auth';
import { LanguagePicker } from '@/features/language';
import { useResponsiveStore } from '@/stores';
import { LanguagePicker } from '../language/';
import { default as IconDocs } from '../assets/icon-docs.svg?url';
import { DropdownMenu } from './DropdownMenu';
import { LaGaufre } from './LaGaufre';
import { default as IconDocs } from './assets/icon-docs.svg?url';
export const HEADER_HEIGHT = '100px';
const RedStripe = styled.div`
position: absolute;
height: 5px;
width: 100%;
background: var(--c--theme--colors--danger-500);
top: 0;
`;
export const Header = () => {
const { t } = useTranslation();
const { themeTokens } = useCunninghamTheme();
const logo = themeTokens().logo;
const { isSmallMobile } = useResponsiveStore();
return (
<Box
as="header"
$justify="center"
$width="100%"
$height={HEADER_HEIGHT}
$zIndex="100"
$padding={{ vertical: 'xtiny' }}
$css="box-shadow: 0 1px 4px #00000040;"
>
<RedStripe />
<Box
$margin={{ horizontal: 'xbig' }}
$margin={{
left: 'big',
right: isSmallMobile ? 'none' : 'big',
}}
$align="center"
$justify="space-between"
$direction="row"
>
<Box $gap="6rem" $direction="row">
{logo && (
<Image
priority
src={logo.src}
alt={logo.alt}
width={0}
height={0}
style={{ width: logo.widthHeader, height: 'auto' }}
/>
)}
<Box>
<StyledLink href="/">
<Box
$align="center"
@@ -63,32 +44,45 @@ export const Header = () => {
$height="fit-content"
$margin={{ top: 'auto' }}
>
<Image priority src={IconDocs} alt={t('Docs Logo')} width={38} />
<Image priority src={IconDocs} alt={t('Docs Logo')} width={25} />
<Text
$padding="3px 5px"
$padding="2px 3px"
$size="8px"
$background="#368bd6"
$color="white"
$position="absolute"
$radius="5px"
$css={`
bottom: 21px;
right: -21px;
bottom: 13px;
right: -17px;
`}
>
BETA
</Text>
<Text $margin="none" as="h2" $theme="primary" $zIndex={1}>
<Text
$margin="none"
as="h2"
$theme="primary"
$zIndex={1}
$size="1.30rem"
>
{t('Docs')}
</Text>
</Box>
</StyledLink>
</Box>
<Box $align="center" $gap="1.5rem" $direction="row">
<AccountDropdown />
<LanguagePicker />
<LaGaufre />
</Box>
{isSmallMobile ? (
<Box $direction="row" $gap="2rem">
<LaGaufre />
<DropdownMenu />
</Box>
) : (
<Box $align="center" $gap="2vw" $direction="row">
<ButtonLogin />
<LanguagePicker />
<LaGaufre />
</Box>
)}
</Box>
</Box>
);

View File

@@ -2,6 +2,13 @@ import { Gaufre } from '@gouvfr-lasuite/integration';
import '@gouvfr-lasuite/integration/dist/css/gaufre.css';
import Script from 'next/script';
import React from 'react';
import { createGlobalStyle } from 'styled-components';
const GaufreStyle = createGlobalStyle`
.lasuite-gaufre-btn{
box-shadow: inset 0 0 0 0 !important;
}
`;
export const LaGaufre = () => (
<>
@@ -10,6 +17,7 @@ export const LaGaufre = () => (
strategy="lazyOnload"
id="lasuite-gaufre-script"
/>
<Gaufre />
<GaufreStyle />
<Gaufre variant="small" />
</>
);

View File

@@ -1 +1 @@
export * from './Header';
export * from './components/Header';

View File

@@ -10,12 +10,12 @@ import IconLanguage from './assets/icon-language.svg?url';
const SelectStyled = styled(Select)<{ $isSmall?: boolean }>`
flex-shrink: 0;
width: 5.5rem;
width: auto;
.c__select__wrapper {
min-height: 2rem;
height: auto;
border-color: #ddd;
border-color: transparent;
padding: 0 0.15rem 0 0.45rem;
border-radius: 1px;
@@ -28,7 +28,7 @@ const SelectStyled = styled(Select)<{ $isSmall?: boolean }>`
}
&:hover {
border-color: var(--c--theme--colors--primary-500);
box-shadow: var(--c--theme--colors--primary-100) 0 0 0 2px !important;
}
}
`;

View File

@@ -1,6 +1,6 @@
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { HEADER_HEIGHT, Header } from '@/features/header';
import { Header } from '@/features/header';
export function MainLayout({ children }: { children: React.ReactNode }) {
const { colorsTokens } = useCunninghamTheme();
@@ -12,7 +12,7 @@ export function MainLayout({ children }: { children: React.ReactNode }) {
<Box $css="flex: 1;" $direction="row">
<Box
as="main"
$minHeight={`calc(100vh - ${HEADER_HEIGHT})`}
$minHeight="100vh"
$width="100%"
$background={colorsTokens()['primary-bg']}
>

View File

@@ -28,6 +28,7 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
)}
/>
<link rel="icon" href="/favicon.ico" sizes="any" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<AppProvider>{getLayout(<Component {...pageProps} />)}</AppProvider>
</>