From cb19a6ee71005db5af7124261d66ee772b3cb518 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 23 Jun 2026 12:34:24 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=9A(frontend)=20move=20Waffle=20to=20b?= =?UTF-8?q?ottom=20left?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We moved the waffle button to the bottom left inside the left panel. This change is part of the new header design. --- CHANGELOG.md | 1 + .../e2e/__tests__/app-impress/header.spec.ts | 84 ----------------- .../__tests__/app-impress/left-panel.spec.ts | 92 ++++++++++++++++++- .../header => }/components/Waffle.tsx | 28 ++---- .../separators/SeparatedSection.tsx | 23 +++-- .../impress/src/core/config/api/useConfig.tsx | 5 +- .../src/features/header/components/Header.tsx | 8 +- .../src/features/header/components/index.ts | 1 - .../src/features/help/components/HelpMenu.tsx | 17 ++++ .../features/home/components/HomeHeader.tsx | 3 +- .../left-panel/components/LeftPanel.tsx | 47 +--------- .../left-panel/components/LeftPanelFooter.tsx | 31 +++++++ 12 files changed, 171 insertions(+), 169 deletions(-) rename src/frontend/apps/impress/src/{features/header => }/components/Waffle.tsx (59%) create mode 100644 src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFooter.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 46affcd12..5b148363a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts index 2e40a4201..2036733d8 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts @@ -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, }) => { diff --git a/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts index c42c2ed57..e3104b4bd 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts @@ -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, }) => { diff --git a/src/frontend/apps/impress/src/features/header/components/Waffle.tsx b/src/frontend/apps/impress/src/components/Waffle.tsx similarity index 59% rename from src/frontend/apps/impress/src/features/header/components/Waffle.tsx rename to src/frontend/apps/impress/src/components/Waffle.tsx index 844685f96..ef9e1e18e 100644 --- a/src/frontend/apps/impress/src/features/header/components/Waffle.tsx +++ b/src/frontend/apps/impress/src/components/Waffle.tsx @@ -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; - 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); + } } `} > - ) => { +}: PropsWithChildren) => { 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} diff --git a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx index eaaee7fad..a8318aea1 100644 --- a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx +++ b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx @@ -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; @@ -38,7 +39,7 @@ interface ThemeCustomization { ready_template_url?: string; }; translations?: Resource; - waffle?: WaffleType; + waffle?: LaGaufreV2Props; } export interface ConfigResponse { diff --git a/src/frontend/apps/impress/src/features/header/components/Header.tsx b/src/frontend/apps/impress/src/features/header/components/Header.tsx index c60e5c0b6..4ce20a093 100644 --- a/src/frontend/apps/impress/src/features/header/components/Header.tsx +++ b/src/frontend/apps/impress/src/features/header/components/Header.tsx @@ -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 = () => { /> - {!isLargeScreen ? ( - - - - ) : ( + {isLargeScreen && ( { > - )} diff --git a/src/frontend/apps/impress/src/features/header/components/index.ts b/src/frontend/apps/impress/src/features/header/components/index.ts index 88d63fdc0..127632913 100644 --- a/src/frontend/apps/impress/src/features/header/components/index.ts +++ b/src/frontend/apps/impress/src/features/header/components/index.ts @@ -1,3 +1,2 @@ export * from './Header'; -export * from './Waffle'; export * from './Title'; diff --git a/src/frontend/apps/impress/src/features/help/components/HelpMenu.tsx b/src/frontend/apps/impress/src/features/help/components/HelpMenu.tsx index 970202d5f..690604c87 100644 --- a/src/frontend/apps/impress/src/features/help/components/HelpMenu.tsx +++ b/src/frontend/apps/impress/src/features/help/components/HelpMenu.tsx @@ -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 && } diff --git a/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx b/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx index a95c9a67c..0c854c1bf 100644 --- a/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx +++ b/src/frontend/apps/impress/src/features/home/components/HomeHeader.tsx @@ -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'; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx index 4534543fc..8770e4f5b 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx @@ -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 ( { - {showHelpMenu && ( - - - - - - )} + ); }; @@ -126,22 +102,7 @@ const LeftPanelMobile = () => { > - - - - - - - - - - + diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFooter.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFooter.tsx new file mode 100644 index 000000000..5c6e762db --- /dev/null +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFooter.tsx @@ -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 ( + + + + + {!isLargeScreen && ( + <> + + + + )} + + + + + ); +};