🚚(frontend) move Waffle to bottom left

We moved the waffle button to the bottom left inside
the left panel.
This change is part of the new header design.
This commit is contained in:
Anthony LC
2026-06-24 16:21:13 +02:00
parent 9605dc7c90
commit cb19a6ee71
12 changed files with 171 additions and 169 deletions
+1
View File
@@ -19,6 +19,7 @@ and this project adheres to
- ♿️(frontend) configurable legal submenu in HelpMenu, remove Crisp #2416
- ♻️(frontend) new create button for docs #2423
- ♿️(frontend) align search modal field label with placeholder #2384
- 🚚(frontend) move Waffle to bottom left #2455
### Fixed
@@ -56,90 +56,6 @@ test.describe('Header', () => {
await expect(header.locator('h1').getByText('Docs')).toHaveClass(/sr-only/);
});
test('checks a custom waffle', async ({ page }) => {
await overrideConfig(page, {
theme_customization: {
waffle: {
data: {
services: [
{
name: 'Docs E2E Custom 1',
url: 'https://docs.numerique.gouv.fr/',
maturity: 'stable',
logo: 'https://lasuite.numerique.gouv.fr/assets/products/docs.svg',
},
{
name: 'Docs E2E Custom 2',
url: 'https://docs.numerique.gouv.fr/',
maturity: 'stable',
logo: 'https://lasuite.numerique.gouv.fr/assets/products/docs.svg',
},
],
},
showMoreLimit: 9,
},
},
});
await page.goto('/');
const header = page.locator('header').first();
await expect(
header.getByRole('button', { name: 'Digital LaSuite services' }),
).toBeVisible();
/**
* The Waffle loads a js file from a remote server,
* it takes some time to load the file and have the interaction available
*/
await page.waitForTimeout(1500);
await header
.getByRole('button', { name: 'Digital LaSuite services' })
.click();
await expect(
page.getByRole('link', { name: 'Docs E2E Custom 1' }),
).toBeVisible();
await expect(
page.getByRole('link', { name: 'Docs E2E Custom 2' }),
).toBeVisible();
});
test('checks the waffle dsfr', async ({ page }) => {
await overrideConfig(page, {
theme_customization: {
waffle: {
apiUrl: 'https://lasuite.numerique.gouv.fr/api/services',
showMoreLimit: 9,
},
},
});
await page.goto('/');
const header = page.locator('header').first();
await expect(
header.getByRole('button', { name: 'Digital LaSuite services' }),
).toBeVisible();
/**
* The Waffle loads a js file from a remote server,
* it takes some time to load the file and have the interaction available
*/
await page.waitForTimeout(1500);
await header
.getByRole('button', {
name: 'Digital LaSuite services',
})
.click();
await expect(page.getByRole('link', { name: 'Tchap' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Grist' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Visio' })).toBeVisible();
});
test('it displays skip link on first TAB and focuses page heading on click', async ({
page,
}) => {
@@ -1,6 +1,11 @@
import { expect, test } from '@playwright/test';
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
import {
createDoc,
goToGridDoc,
overrideConfig,
verifyDocName,
} from './utils-common';
import { tryFocusEditorContent } from './utils-editor';
import { createRootSubPage } from './utils-sub-pages';
@@ -20,6 +25,91 @@ test.describe('Left panel desktop', () => {
await expect(page.getByTestId('home-button')).toBeVisible();
});
test('checks a custom waffle', async ({ page }) => {
await overrideConfig(page, {
theme_customization: {
waffle: {
data: {
services: [
{
name: 'Docs E2E Custom 1',
url: 'https://docs.numerique.gouv.fr/',
maturity: 'stable',
logo: 'https://lasuite.numerique.gouv.fr/assets/products/docs.svg',
},
{
name: 'Docs E2E Custom 2',
url: 'https://docs.numerique.gouv.fr/',
maturity: 'stable',
logo: 'https://lasuite.numerique.gouv.fr/assets/products/docs.svg',
},
],
},
showMoreLimit: 9,
},
},
});
await page.goto('/');
const leftPanel = page.getByTestId('left-panel-desktop');
await expect(
leftPanel.getByRole('button', { name: 'Digital LaSuite services' }),
).toBeVisible();
/**
* The Waffle loads a js file from a remote server,
* it takes some time to load the file and have the interaction available
*/
await page.waitForTimeout(1500);
await leftPanel
.getByRole('button', { name: 'Digital LaSuite services' })
.click();
await expect(
page.getByRole('link', { name: 'Docs E2E Custom 1' }),
).toBeVisible();
await expect(
page.getByRole('link', { name: 'Docs E2E Custom 2' }),
).toBeVisible();
});
test('checks the waffle dsfr', async ({ page }) => {
await overrideConfig(page, {
theme_customization: {
waffle: {
apiUrl: 'https://lasuite.numerique.gouv.fr/api/services',
showMoreLimit: 9,
},
},
});
await page.goto('/');
const leftPanel = page.getByTestId('left-panel-desktop');
await expect(
leftPanel.getByRole('button', { name: 'Digital LaSuite services' }),
).toBeVisible();
/**
* The Waffle loads a js file from a remote server,
* it takes some time to load the file and have the interaction available
*/
await page.waitForTimeout(1500);
await leftPanel
.getByRole('button', {
name: 'Digital LaSuite services',
})
.click();
await expect(page.getByRole('link', { name: 'Tchap' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Grist' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Visio' })).toBeVisible();
});
test('focuses page heading after switching the docs filter', async ({
page,
}) => {
@@ -1,4 +1,4 @@
import { LaGaufreV2, LaGaufreV2Props } from '@gouvfr-lasuite/ui-kit';
import { LaGaufreV2 } from '@gouvfr-lasuite/ui-kit';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -6,26 +6,6 @@ import { css } from 'styled-components';
import { Box } from '@/components';
import { useConfig } from '@/core';
type WaffleAPIType = {
apiUrl: LaGaufreV2Props['apiUrl'];
data?: never;
};
type WaffleDataType = {
apiUrl?: never;
data?: LaGaufreV2Props['data'];
};
export type WaffleType = Omit<
LaGaufreV2Props,
'apiUrl' | 'data' | 'widgetPath'
> &
(WaffleAPIType | WaffleDataType) & {
widgetPath?: string;
};
const LaGaufreV2Fixed = LaGaufreV2 as React.ComponentType<WaffleType>;
export const Waffle = () => {
const { t } = useTranslation();
const { data: conf } = useConfig();
@@ -41,10 +21,14 @@ export const Waffle = () => {
$css={css`
& > div {
display: flex;
.c__button--brand--tertiary svg path {
fill: var(--c--contextuals--content--semantic--neutral--tertiary);
}
}
`}
>
<LaGaufreV2Fixed
<LaGaufreV2
{...waffleConfig}
label={waffleConfig.label ?? t('Digital LaSuite services')}
newWindowLabelSuffix={` (${t('new window')})`}
@@ -5,14 +5,14 @@ import { useCunninghamTheme } from '@/cunningham';
import { Box } from '../Box';
type Props = {
showSeparator?: boolean;
type SeparatedSectionProps = {
showSeparator?: 'top' | 'bottom' | boolean;
};
export const SeparatedSection = ({
showSeparator = true,
children,
}: PropsWithChildren<Props>) => {
}: PropsWithChildren<SeparatedSectionProps>) => {
const { spacingsTokens } = useCunninghamTheme();
return (
@@ -20,11 +20,18 @@ export const SeparatedSection = ({
$css={css`
width: 100%;
padding: ${spacingsTokens['sm']} 0;
${showSeparator &&
css`
border-bottom: 1px solid
var(--c--contextuals--border--surface--primary);
`}
${showSeparator === 'top' || showSeparator === true
? css`
border-top: 1px solid
var(--c--contextuals--border--surface--primary);
`
: ''}
${showSeparator === 'bottom' || showSeparator === true
? css`
border-bottom: 1px solid
var(--c--contextuals--border--surface--primary);
`
: ''}
`}
>
{children}
@@ -1,3 +1,4 @@
import { LaGaufreV2Props } from '@gouvfr-lasuite/ui-kit';
import { useQuery } from '@tanstack/react-query';
import type { Resource } from 'i18next';
import Image from 'next/image';
@@ -6,7 +7,7 @@ import type { LinkHTMLAttributes } from 'react';
import { APIError, errorCauses, fetchAPI } from '@/api';
import type { Theme } from '@/cunningham/';
import type { FooterType } from '@/features/footer';
import type { HeaderType, WaffleType } from '@/features/header';
import type { HeaderType } from '@/features/header';
import type { PostHogConf } from '@/services/PosthogAnalytic';
type Imagetype = React.ComponentProps<typeof Image>;
@@ -38,7 +39,7 @@ interface ThemeCustomization {
ready_template_url?: string;
};
translations?: Resource;
waffle?: WaffleType;
waffle?: LaGaufreV2Props;
}
export interface ConfigResponse {
@@ -13,7 +13,6 @@ import { useResponsiveStore } from '@/stores';
import { HEADER_HEIGHT } from '../conf';
import { Title } from './Title';
import { Waffle } from './Waffle';
export const Header = () => {
const { t } = useTranslation();
@@ -82,11 +81,7 @@ export const Header = () => {
/>
</Box>
</StyledLink>
{!isLargeScreen ? (
<Box $direction="row" $gap={spacingsTokens['sm']}>
<Waffle />
</Box>
) : (
{isLargeScreen && (
<Box
className="--docs--header-block-right"
$align="center"
@@ -95,7 +90,6 @@ export const Header = () => {
>
<ButtonLogin />
<LanguagePicker />
<Waffle />
</Box>
)}
</Box>
@@ -1,3 +1,2 @@
export * from './Header';
export * from './Waffle';
export * from './Title';
@@ -39,6 +39,19 @@ export const HelpMenu = ({
const documentationUrl = config?.theme_customization?.help?.documentation_url;
const supportMailto = config?.theme_customization?.help?.support_mailto;
const legalLinks = config?.theme_customization?.help?.legal_links;
/**
* The onboarding can be disable, so we need to check if it's enabled before displaying the help menu.
* TODO: As soon as we get more than one fixed element in the help menu,
* we should remove this condition and display the help menu even if the onboarding is disabled
*/
const showHelpMenu =
onboardingEnabled ||
!!documentationUrl ||
!!supportMailto ||
!!legalLinks?.personal_data ||
!!legalLinks?.terms_of_use ||
!!legalLinks?.accessibility_statement ||
!!legalLinks?.legal_notice;
const toggleMenu = useCallback(() => {
setIsMenuOpen((open) => !open);
@@ -143,6 +156,10 @@ export const HelpMenu = ({
],
);
if (!showHelpMenu) {
return null;
}
return (
<>
{isMenuOpen && <HelpMenuStyle />}
@@ -1,9 +1,10 @@
import Image from 'next/image';
import { Box } from '@/components';
import { Waffle } from '@/components/Waffle';
import { useConfig } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { Title, Waffle } from '@/features/header';
import { Title } from '@/features/header';
import { LanguagePicker } from '@/features/language';
import { LeftPanelToggleMobile } from '@/features/left-panel';
import { useResponsiveStore } from '@/stores';
@@ -3,18 +3,15 @@ import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { createGlobalStyle, css } from 'styled-components';
import { Box, HorizontalSeparator, SeparatedSection } from '@/components';
import { useConfig } from '@/core/config/api/useConfig';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { ButtonLogin } from '@/features/auth';
import { HEADER_HEIGHT } from '@/features/header/conf';
import { HelpMenu } from '@/features/help';
import { LanguagePicker } from '@/features/language';
import { useResponsiveStore } from '@/stores';
import { useLeftPanelStore } from '../stores';
import { LeftPanelContent } from './LeftPanelContent';
import { LeftPanelFooter } from './LeftPanelFooter';
import { LeftPanelHeader } from './LeftPanelHeader';
const MobileLeftPanelStyle = createGlobalStyle`
@@ -34,21 +31,6 @@ export const LeftPanel = () => {
export const LeftPanelDesktop = () => {
const { t } = useTranslation();
const { data: config } = useConfig();
const legalLinks = config?.theme_customization?.help?.legal_links;
/**
* The onboarding can be disable, so we need to check if it's enabled before displaying the help menu.
* TODO: As soon as we get more than one fixed element in the help menu,
* we should remove this condition and display the help menu even if the onboarding is disabled
*/
const showHelpMenu =
config?.theme_customization?.onboarding?.enabled ||
!!config?.theme_customization?.help?.documentation_url ||
!!config?.theme_customization?.help?.support_mailto ||
!!legalLinks?.personal_data ||
!!legalLinks?.terms_of_use ||
!!legalLinks?.accessibility_statement ||
!!legalLinks?.legal_notice;
return (
<Box
@@ -71,13 +53,7 @@ export const LeftPanelDesktop = () => {
<LeftPanelHeader />
</Box>
<LeftPanelContent />
{showHelpMenu && (
<SeparatedSection showSeparator={false}>
<Box $padding={{ horizontal: 'sm' }} $justify="flex-start">
<HelpMenu />
</Box>
</SeparatedSection>
)}
<LeftPanelFooter />
</Box>
);
};
@@ -126,22 +102,7 @@ const LeftPanelMobile = () => {
>
<LeftPanelHeader />
<LeftPanelContent />
<Box $width="100%">
<HorizontalSeparator $margin="none" />
<SeparatedSection showSeparator={false}>
<Box
$justify="end"
$align="center"
$gap={spacingsTokens['xs']}
$direction="row"
$padding={{ horizontal: 'sm' }}
>
<HelpMenu colorButton="brand" />
<ButtonLogin />
<LanguagePicker />
</Box>
</SeparatedSection>
</Box>
<LeftPanelFooter />
</Box>
</Box>
</>
@@ -0,0 +1,31 @@
import { Box, SeparatedSection } from '@/components';
import { Waffle } from '@/components/Waffle';
import { ButtonLogin } from '@/features/auth';
import { HelpMenu } from '@/features/help';
import { LanguagePicker } from '@/features/language';
import { useResponsiveStore } from '@/stores';
export const LeftPanelFooter = () => {
const { isLargeScreen } = useResponsiveStore();
return (
<SeparatedSection showSeparator="top">
<Box
$padding={{ horizontal: 'sm' }}
$justify="space-between"
$direction="row"
>
<Box $direction="row">
<Waffle />
{!isLargeScreen && (
<>
<ButtonLogin />
<LanguagePicker />
</>
)}
</Box>
<HelpMenu />
</Box>
</SeparatedSection>
);
};