diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3b5e6f02..50f612b167 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to - ♿(frontend) improve accessibility: - ♿(frontend) improve share modal button accessibility #1626 +### Added + +- ♿(frontend) improve accessibility: + - ♿(frontend) add skip to content button for keyboard accessibility #1624 + ## [3.10.0] - 2025-11-18 ### Added @@ -30,6 +35,9 @@ and this project adheres to - ♿(frontend) improve ARIA in doc grid and editor for a11y #1519 - ♿(frontend) improve accessibility and styling of summary table #1528 - ♿(frontend) add focus trap and enter key support to remove doc modal #1531 +- 🐛(frontend) preserve @ character when esc is pressed after typing it #1512 +- 🐛(frontend) make summary button fixed to remain visible during scroll #1581 +- 🐛(frontend) fix pdf embed to use full width #1526 - 🐛(frontend) fix alignment of side menu #1597 - 🐛(frontend) fix fallback translations with Trans #1620 - 🐛(export) fix image overflow by limiting width to 600px during export #1525 @@ -46,7 +54,6 @@ and this project adheres to - 🔥(backend) remove api managing templates - ## [3.9.0] - 2025-11-10 ### Added 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 349af4e1c7..b89d8bf3df 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts @@ -176,3 +176,27 @@ test.describe('Header: Override configuration', () => { await expect(logoImage).toHaveAttribute('alt', ''); }); }); + +test.describe('Header: Skip to Content', () => { + test('it displays skip link on first TAB and focuses main content on click', async ({ + page, + }) => { + await page.goto('/'); + + // Wait for skip link to be mounted (client-side only component) + const skipLink = page.getByRole('link', { name: 'Go to content' }); + await skipLink.waitFor({ state: 'attached' }); + + // First TAB shows the skip link + await page.keyboard.press('Tab'); + + // The skip link should be visible and focused + await expect(skipLink).toBeFocused(); + await expect(skipLink).toBeVisible(); + + // Clicking moves focus to the main content + await skipLink.click(); + const mainContent = page.locator('main#mainContent'); + await expect(mainContent).toBeFocused(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts index 6bfc37fd22..127caeb7ce 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts @@ -66,6 +66,7 @@ test.describe('Language', () => { await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); await page.keyboard.press('Enter'); diff --git a/src/frontend/apps/impress/src/components/SkipToContent.tsx b/src/frontend/apps/impress/src/components/SkipToContent.tsx new file mode 100644 index 0000000000..257bcd9ea0 --- /dev/null +++ b/src/frontend/apps/impress/src/components/SkipToContent.tsx @@ -0,0 +1,63 @@ +import { Button } from '@openfun/cunningham-react'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { MAIN_LAYOUT_ID } from '@/layouts/conf'; + +export const SkipToContent = () => { + const { t } = useTranslation(); + const router = useRouter(); + const [isVisible, setIsVisible] = useState(false); + + // Reset focus after route change so first TAB goes to skip link + useEffect(() => { + const handleRouteChange = () => { + (document.activeElement as HTMLElement)?.blur(); + + document.body.setAttribute('tabindex', '-1'); + document.body.focus({ preventScroll: true }); + + setTimeout(() => { + document.body.removeAttribute('tabindex'); + }, 100); + }; + + router.events.on('routeChangeComplete', handleRouteChange); + return () => { + router.events.off('routeChangeComplete', handleRouteChange); + }; + }, [router.events]); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + const mainContent = document.getElementById(MAIN_LAYOUT_ID); + if (mainContent) { + mainContent.focus(); + mainContent.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }; + + return ( + + ); +}; diff --git a/src/frontend/apps/impress/src/components/index.ts b/src/frontend/apps/impress/src/components/index.ts index 89916d0fe8..904c11bada 100644 --- a/src/frontend/apps/impress/src/components/index.ts +++ b/src/frontend/apps/impress/src/components/index.ts @@ -11,5 +11,6 @@ export * from './Loading'; export * from './modal'; export * from './Overlayer'; export * from './separators'; +export * from './SkipToContent'; export * from './Text'; export * from './TextErrors'; 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 3ab656615b..a23d314b68 100644 --- a/src/frontend/apps/impress/src/features/header/components/Header.tsx +++ b/src/frontend/apps/impress/src/features/header/components/Header.tsx @@ -2,7 +2,7 @@ import Image from 'next/image'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; -import { Box, StyledLink } from '@/components/'; +import { Box, SkipToContent, StyledLink } from '@/components/'; import { useConfig } from '@/core/config'; import { useCunninghamTheme } from '@/cunningham'; import { ButtonLogin } from '@/features/auth'; @@ -24,73 +24,76 @@ export const Header = () => { const logo = config?.theme_customization?.header?.logo; return ( - - {!isDesktop && } - + + - } + - -