From ad33e9e0eba2240173390298f4c4c7b604df6918 Mon Sep 17 00:00:00 2001 From: Cyril Date: Wed, 3 Jun 2026 11:59:39 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BF=EF=B8=8F(frontend)=20use=20anchor=20l?= =?UTF-8?q?inks=20for=20table=20of=20contents=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace TOC buttons with anchor links and heading ids for a11y navigation. --- CHANGELOG.md | 1 + .../app-impress/doc-table-content.spec.ts | 33 ++++++---- .../docs/doc-editor/hook/useHeadings.tsx | 1 - .../doc-table-content/components/Heading.tsx | 23 ++++--- .../components/TableContentSideBar.tsx | 61 +++++++++++-------- 5 files changed, 74 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d5d108011..179c9e58d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to - 👷(CI) remove test-e2e-other-browser job #2404 - ♿️(frontend) use heading element for pinned documents section title #2380 +- ♿️(frontend) use anchor links for table of contents entries #2390 ### Fixed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts index e690385944..3f1766f325 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-table-content.spec.ts @@ -32,34 +32,43 @@ test.describe('Doc Table Content', () => { const elSidePanel = page.getByLabel('Table of contents side panel'); - const level1 = elSidePanel.getByText('Level 1'); + const link1 = elSidePanel.getByRole('link', { name: 'Level 1' }); + const link2 = elSidePanel.getByRole('link', { name: 'Level 2' }); + const link3 = elSidePanel.getByRole('link', { name: 'Level 3' }); + const level1 = link1.getByText('Level 1'); const editorLevel1 = editor.getByText('Level 1'); - const level2 = elSidePanel.getByText('Level 2'); + const level2 = link2.getByText('Level 2'); const editorLevel2 = editor.getByText('Level 2'); - const level3 = elSidePanel.getByText('Level 3'); + const level3 = link3.getByText('Level 3'); + + // TOC entries must be links with fragment hrefs + await expect(link1).toBeVisible(); + await expect(link1).toHaveAttribute('href', /^#heading-/); + await expect(link2).toHaveAttribute('href', /^#heading-/); + await expect(link3).toHaveAttribute('href', /^#heading-/); await expect(level1).toBeVisible(); await expect(editorLevel1).not.toBeInViewport(); - await expect(level1).toHaveAttribute('aria-selected', 'false'); + await expect(link1).not.toHaveAttribute('aria-current'); await expect(level2).toBeVisible(); await expect(level2).toHaveCSS('padding-left', /14.4px/); await expect(editorLevel2).toBeInViewport(); - await expect(level2).toHaveAttribute('aria-selected', 'true'); + await expect(link2).toHaveAttribute('aria-current', 'true'); await expect(level3).toBeVisible(); await expect(level3).toHaveCSS('padding-left', /24px/); - await expect(level3).toHaveAttribute('aria-selected', 'false'); + await expect(link3).not.toHaveAttribute('aria-current'); - await level1.click(); + await link1.click(); await expect(editorLevel1).toBeInViewport(); - await expect(level1).toHaveAttribute('aria-selected', 'true'); - await expect(level2).toHaveAttribute('aria-selected', 'false'); + await expect(link1).toHaveAttribute('aria-current', 'true'); + await expect(link2).not.toHaveAttribute('aria-current'); - await level2.click(); + await link2.click(); await expect(editorLevel1).not.toBeInViewport(); await expect(editorLevel2).toBeInViewport(); - await expect(level2).toHaveAttribute('aria-selected', 'true'); - await expect(level1).toHaveAttribute('aria-selected', 'false'); + await expect(link2).toHaveAttribute('aria-current', 'true'); + await expect(link1).not.toHaveAttribute('aria-current'); }); }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx index 10f3970033..83d107eb1c 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx @@ -7,7 +7,6 @@ export const useHeadings = (editor: DocsBlockNoteEditor) => { const { setHeadings, resetHeadings } = useHeadingStore(); useEffect(() => { - // Check if editor and its view are mounted before accessing document if (!editor) { return; } diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx index 5aa6db99c8..9ceb987da0 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/Heading.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { css } from 'styled-components'; -import { BoxButton, Text } from '@/components'; +import { Box, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; import { DocsBlockNoteEditor } from '@/docs/doc-editor/types'; import { useResponsiveStore } from '@/stores'; @@ -38,15 +38,18 @@ export const Heading = ({ const isActive = isHighlight || isHover; return ( - setIsHover(true)} onMouseLeave={() => setIsHover(false)} - onClick={() => { + onClick={(e: React.MouseEvent) => { // With mobile the focus open the keyboard and the scroll is not working + e.preventDefault(); + if (!isMobile) { editor.focus(); } @@ -68,17 +71,22 @@ export const Heading = ({ : 'none' } $justify="center" + $padding="none" + $margin="none" + $hasTransition $css={css` text-align: left; + display: flex; + text-decoration: none; + color: inherit; + cursor: pointer; &:focus-visible { - /* Scoped focus style: same footprint as hover, with theme shadow */ outline: none; box-shadow: 0 0 0 2px ${colorsTokens['brand-400']}; border-radius: var(--c--globals--spacings--st); } `} aria-label={text} - aria-selected={isHighlight} aria-current={isHighlight ? 'true' : undefined} > {text} - + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContentSideBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContentSideBar.tsx index dbf8afd028..c23b3047ed 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContentSideBar.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContentSideBar.tsx @@ -95,7 +95,13 @@ export const TableContentSideBar = ({ onClose }: TableContentSideBarProps) => { `} > - + {t('Table of Contents')} { {editor && headings && headings.length > 0 && ( - {headings.map( - (heading) => - heading.contentText && ( - - - - ), - )} + + {headings.map( + (heading) => + heading.contentText && ( + + + + ), + )} + )}