-
Notifications
You must be signed in to change notification settings - Fork 598
♿️(frontend) use anchor links for table of contents entries #2390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 <a> 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'); | ||
| }); | ||
|
Comment on lines
+63
to
73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Assert focus transfer after TOC activation. On Line 64 and Line 69, clicks currently validate viewport and Suggested test diff@@
await expect(link1).toHaveAttribute('href', /^`#heading-/`);
await expect(link2).toHaveAttribute('href', /^`#heading-/`);
await expect(link3).toHaveAttribute('href', /^`#heading-/`);
+ const heading1Anchor = page.locator((await link1.getAttribute('href'))!);
+ const heading2Anchor = page.locator((await link2.getAttribute('href'))!);
@@
await link1.click();
await expect(editorLevel1).toBeInViewport();
+ await expect(heading1Anchor).toBeFocused();
await expect(link1).toHaveAttribute('aria-current', 'true');
await expect(link2).not.toHaveAttribute('aria-current');
@@
await link2.click();
await expect(editorLevel1).not.toBeInViewport();
await expect(editorLevel2).toBeInViewport();
+ await expect(heading2Anchor).toBeFocused();
await expect(link2).toHaveAttribute('aria-current', 'true');
await expect(link1).not.toHaveAttribute('aria-current');🤖 Prompt for AI Agents |
||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <BoxButton | ||
| id={`heading-${headingId}`} | ||
| <Box | ||
| as="a" | ||
| href={`#heading-${headingId}`} | ||
| className="--docs--table-content-heading" | ||
| $width="100%" | ||
| $minHeight="var(--c--globals--spacings--lg)" | ||
| onMouseOver={() => setIsHover(true)} | ||
| onMouseLeave={() => setIsHover(false)} | ||
| onClick={() => { | ||
| onClick={(e: React.MouseEvent<HTMLAnchorElement>) => { | ||
| // With mobile the focus open the keyboard and the scroll is not working | ||
|
Ovgodd marked this conversation as resolved.
|
||
| 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} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win Prefer Per the ARIA specification, ♻️ Proposed fix- aria-current={isHighlight ? 'true' : undefined}
+ aria-current={isHighlight ? 'location' : undefined}🤖 Prompt for AI Agents |
||
| > | ||
| <Text | ||
|
|
@@ -87,10 +95,9 @@ export const Heading = ({ | |
| $weight={isHighlight ? '700' : '500'} | ||
| $css="overflow-wrap: break-word;" | ||
| $hasTransition | ||
| aria-selected={isHighlight} | ||
| > | ||
| {text} | ||
| </Text> | ||
| </BoxButton> | ||
| </Box> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add URL hash verification to anchor link tests.
The test verifies viewport scrolling and
aria-currentstate, but doesn't verify that clicking TOC links updates the URL hash (e.g.,expect(page.url()).toContain('#heading-')). This is a core anchor link behavior and should be tested.🧪 Proposed test enhancement
await link1.click(); await expect(editorLevel1).toBeInViewport(); + await expect(page).toHaveURL(/`#heading-`/); await expect(link1).toHaveAttribute('aria-current', 'location'); await expect(link2).not.toHaveAttribute('aria-current'); await link2.click(); await expect(editorLevel1).not.toBeInViewport(); await expect(editorLevel2).toBeInViewport(); + await expect(page).toHaveURL(/`#heading-`/); await expect(link2).toHaveAttribute('aria-current', 'location'); await expect(link1).not.toHaveAttribute('aria-current');Note: This test would currently fail due to the
preventDefault()call inHeading.tsxthat blocks URL hash updates.🤖 Prompt for AI Agents