From 5344155aa10503a3c7c82a905483b8b79b12447b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 12 Nov 2026 18:42:19 -0800 Subject: [PATCH 1/2] fix(docs): copy section permalinks and polish outline navigation --- .../components/docs/table-of-contents.tsx | 22 +++- apps/docs/components/ui/heading-link.tsx | 53 +++++++++ apps/docs/components/ui/heading.tsx | 12 +- apps/docs/scripts/test-navigation-e2e.ts | 112 ++++++++++++++++++ 4 files changed, 187 insertions(+), 12 deletions(-) create mode 100644 apps/docs/components/ui/heading-link.tsx diff --git a/apps/docs/components/docs/table-of-contents.tsx b/apps/docs/components/docs/table-of-contents.tsx index 4f4b5289e3c..fb6da145b37 100644 --- a/apps/docs/components/docs/table-of-contents.tsx +++ b/apps/docs/components/docs/table-of-contents.tsx @@ -19,9 +19,18 @@ export function TableOfContents({ headings, active, compact = false }: TableOfCo {headings.map(({ id, title, depth }) => (
  • { + if ( + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) + return const details = event.currentTarget.closest('details') if (details) details.open = false }} @@ -40,7 +49,16 @@ export function TableOfContents({ headings, active, compact = false }: TableOfCo ) return compact ? ( -
    +
    { + if (event.key !== 'Escape' || !event.currentTarget.open) return + event.preventDefault() + event.stopPropagation() + event.currentTarget.open = false + event.currentTarget.querySelector('summary')?.focus() + }} + > On this page diff --git a/apps/docs/components/ui/heading-link.tsx b/apps/docs/components/ui/heading-link.tsx new file mode 100644 index 00000000000..052a5de1a1e --- /dev/null +++ b/apps/docs/components/ui/heading-link.tsx @@ -0,0 +1,53 @@ +'use client' + +import { useState } from 'react' +import { useCopyToClipboard } from '@sim/emcn' +import { Check, Link, TriangleAlert } from '@sim/emcn/icons' +import { cn } from '@/components/docs/classnames' + +interface HeadingLinkProps { + id: string +} + +/** Copies a section permalink while retaining native anchor navigation. */ +export function HeadingLink({ id }: HeadingLinkProps) { + const { copied, copy } = useCopyToClipboard() + const [failed, setFailed] = useState(false) + const announcement = failed + ? 'Could not copy. Copy the link from the address bar.' + : copied + ? 'Link copied' + : '' + const Icon = failed ? TriangleAlert : copied ? Check : Link + + return ( + <> + { + if ( + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) + return + setFailed(!(await copy(event.currentTarget.href))) + }} + className={cn( + 'pointer-coarse:before:-inset-3.5 relative ml-2 inline-flex rounded align-middle text-[var(--docs-muted)] opacity-0 pointer-coarse:opacity-100 transition-opacity pointer-coarse:before:absolute hover:text-[var(--docs-link)] focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-[var(--docs-link)] focus-visible:outline-offset-2 group-hover/heading:opacity-100 motion-reduce:transition-none', + (copied || failed) && 'opacity-100' + )} + > + + + + {announcement} + + + ) +} diff --git a/apps/docs/components/ui/heading.tsx b/apps/docs/components/ui/heading.tsx index 2aa4d508e6f..d91a8af80d3 100644 --- a/apps/docs/components/ui/heading.tsx +++ b/apps/docs/components/ui/heading.tsx @@ -1,6 +1,6 @@ import type { HTMLAttributes } from 'react' -import { Link } from '@sim/emcn/icons' import { cn } from '@/components/docs/classnames' +import { HeadingLink } from '@/components/ui/heading-link' type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' interface HeadingProps extends HTMLAttributes { @@ -29,15 +29,7 @@ export function Heading({ as: Tag = 'h2', className, children, id, ...props }: H )} > {children} - {id && ( - - - - )} + {id && } ) } diff --git a/apps/docs/scripts/test-navigation-e2e.ts b/apps/docs/scripts/test-navigation-e2e.ts index e6842fb0036..e5ffff3c4c1 100644 --- a/apps/docs/scripts/test-navigation-e2e.ts +++ b/apps/docs/scripts/test-navigation-e2e.ts @@ -178,6 +178,118 @@ try { ? await (engine === 'webkit' ? webkit : chromium).launch() : await webkit.connect(webkitEndpoint, { timeout: 180_000 }) const feedbackBrowser = browser + await check( + 'Section permalinks copy the full URL and preserve native link navigation', + async () => { + const context = await feedbackBrowser.newContext({ + permissions: ['clipboard-read', 'clipboard-write'], + viewport: { width: 1440, height: 900 }, + }) + try { + const page = await context.newPage() + await open(page, '/files/editor?view=reference') + const formatting = page.locator('#formatting-text').getByRole('link') + await formatting.click() + const first = new URL('/files/editor?view=reference#formatting-text', target).href + await expect(page).toHaveURL(first) + await expect(formatting).toHaveAccessibleName('Link copied') + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(first) + + const tables = page.locator('#tables').getByRole('link') + await tables.focus() + await page.keyboard.press('Enter') + const second = new URL('/files/editor?view=reference#tables', target).href + await expect(page).toHaveURL(second) + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(second) + const [newTab] = await Promise.all([ + context.waitForEvent('page', { timeout: 10_000 }), + page + .locator('#code-blocks') + .getByRole('link') + .click({ modifiers: ['ControlOrMeta'] }), + ]) + await newTab.waitForLoadState('domcontentloaded') + await expect(newTab).toHaveURL( + new URL('/files/editor?view=reference#code-blocks', target).href + ) + await newTab.close() + await page.bringToFront() + await expect(page).toHaveURL(second) + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(second) + await page.screenshot({ path: join(directory, 'section-permalink.png') }) + } finally { + await context.close() + } + }, + engine === 'webkit' + ? 'Playwright WebKit cannot grant clipboard read/write permissions; Chromium verifies native clipboard contents.' + : undefined + ) + await check('Section permalink navigation survives an unavailable clipboard', async () => { + const context = await feedbackBrowser.newContext() + try { + await context.addInitScript(() => { + Object.defineProperty(navigator, 'clipboard', { value: undefined }) + }) + const page = await context.newPage() + await open(page, '/files/editor') + const link = page.locator('#formatting-text').getByRole('link') + await link.click() + await expect(page).toHaveURL(new URL('/files/editor#formatting-text', target).href) + await expect(link).toHaveAccessibleName('Could not copy. Copy the link from the address bar.') + await expect(page.getByRole('status').filter({ hasText: 'Could not copy.' })).toBeVisible() + await page.screenshot({ path: join(directory, 'section-permalink-copy-unavailable.png') }) + await page + .getByRole('navigation', { name: 'Page navigation', exact: true }) + .getByRole('link', { name: 'Previous Files', exact: true }) + .click() + await expect(page).toHaveURL(new URL('/files', target).href) + await expect( + page.getByRole('link', { name: 'Could not copy. Copy the link from the address bar.' }) + ).toHaveCount(0) + } finally { + await context.close() + } + }) + await check( + 'Compact page outline dismisses with Escape and preserves new-tab clicks', + async () => { + for (const width of [390, 1100]) { + const context = await feedbackBrowser.newContext({ viewport: { width, height: 900 } }) + try { + const page = await context.newPage() + await open(page, '/files/editor') + const summary = page + .locator('summary') + .filter({ hasText: 'On this page' }) + .filter({ visible: true }) + const details = summary.locator('..') + await summary.click() + await expect(details).toHaveAttribute('open', '') + await page.keyboard.press('Escape') + await expect(details).not.toHaveAttribute('open') + await expect(summary).toBeFocused() + + await summary.click() + const link = details.getByRole('link', { name: 'Formatting text', exact: true }) + const [newTab] = await Promise.all([ + context.waitForEvent('page', { timeout: 10_000 }), + link.click({ modifiers: ['ControlOrMeta'] }), + ]) + await newTab.waitForLoadState('domcontentloaded') + await expect(newTab).toHaveURL(new URL('/files/editor#formatting-text', target).href) + await expect(details).toHaveAttribute('open', '') + await newTab.close() + await link.click() + await expect(page).toHaveURL(new URL('/files/editor#formatting-text', target).href) + await expect(details).not.toHaveAttribute('open') + await page.screenshot({ path: join(directory, `section-outline-${width}.png`) }) + } finally { + await context.close() + } + } + } + ) await check('Feedback forwards votes and preserves unchanged retry identity', async () => { const cert = process.env.DOCS_FEEDBACK_TEST_CERT const key = process.env.DOCS_FEEDBACK_TEST_KEY From dd244b00240ff64ce4a693573ad9afb542ea131e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 12 Nov 2026 18:54:34 -0800 Subject: [PATCH 2/2] test(docs): verify focus returns from outline links --- apps/docs/scripts/test-navigation-e2e.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/scripts/test-navigation-e2e.ts b/apps/docs/scripts/test-navigation-e2e.ts index e5ffff3c4c1..911d3ec98f5 100644 --- a/apps/docs/scripts/test-navigation-e2e.ts +++ b/apps/docs/scripts/test-navigation-e2e.ts @@ -266,6 +266,7 @@ try { const details = summary.locator('..') await summary.click() await expect(details).toHaveAttribute('open', '') + await details.getByRole('link', { name: 'Formatting text', exact: true }).focus() await page.keyboard.press('Escape') await expect(details).not.toHaveAttribute('open') await expect(summary).toBeFocused()