📱(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:
@@ -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: [] } });
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './AccountDropdown';
|
||||
export * from './api/types';
|
||||
export * from './Auth';
|
||||
export * from './ButtonLogin';
|
||||
export * from './useAuthStore';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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 |
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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" />
|
||||
</>
|
||||
);
|
||||
@@ -1 +1 @@
|
||||
export * from './Header';
|
||||
export * from './components/Header';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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']}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user