Skip to content

fix(scroll): keyboard-focusable scroll regions + CodeBlock copy/nesting (complex-6/7/8/18)#3369

Merged
cixzhang merged 1 commit into
mainfrom
navi/fix/scrollable-region-focusable
Jul 2, 2026
Merged

fix(scroll): keyboard-focusable scroll regions + CodeBlock copy/nesting (complex-6/7/8/18)#3369
cixzhang merged 1 commit into
mainfrom
navi/fix/scrollable-region-focusable

Conversation

@cixzhang

@cixzhang cixzhang commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Part of the accessibility & keyboard-management program (#3343). Fixes complex-6, complex-7, complex-8, complex-18.

Problem

Several scroll containers in the design system were not reachable by keyboard, and CodeBlock had a nested-interactive ARIA bug:

  • complex-6 (CodeBlock) — the collapsible header was a div role="button" that contained the Copy <button> (invalid nested interactive controls). The Copy button's onClick also didn't stop propagation, so clicking Copy collapsed/expanded the block.
  • complex-7 (CodeBlock) — the code scroll container (overflow: auto + maxHeight) had no tabIndex, so keyboard users could not scroll long or wide code.
  • complex-8 (Table)TableScrollWrapper (overflow-x: auto) had no tabIndex, so a horizontally overflowing table was not keyboard-scrollable.
  • complex-18 (Markdown) — the GFM table wrapper (overflow-x: auto) had no tabIndex, same problem.

Fix

  • CodeBlock copy/nesting: added e.stopPropagation() to the Copy button handler so copying no longer toggles collapse. Restructured the header into a headerRow flex wrapper that holds the sticky/background/padding/divider layout and lays out two siblings: the collapsible title control (role="button") and the Copy button. The Copy button is no longer a DOM descendant of the interactive header. Visual layout (space-between, sticky header, chevron, divider) is preserved.
  • Scroll regions: added tabIndex={0} + role="region" + an aria-label to each overflow container:
    • CodeBlock scroll container — aria-label is the language label, or "Code" when no language is shown.
    • Table TableScrollWrapperaria-label="Table" (callers can still override role/aria-label via htmlProps).
    • Markdown GFM table wrapper — aria-label="Table".

Decision: unconditional tabIndex

These components do not already have a ref + ResizeObserver wired up to cheaply detect actual overflow, so I used unconditional tabIndex={0} on the scroll containers. This matches the findings' explicit ask and keeps the change minimal and consistent across the three components. A future enhancement could make tabIndex conditional on measured overflow (as Base UI's ScrollArea does) to avoid a tab stop when content fits.

Note on Markdown table nesting

The Markdown GFM table renders the core <Table>, whose TableScrollWrapper is now itself a focusable region. The outer Markdown tableWrapper div still has its own overflow-x: auto, so it is treated per the finding for consistency. This does create two nested focusable regions around the same table; flagging for reviewer awareness in case we'd prefer to collapse to a single scroll layer here.

Tests

  • CodeBlock (CodeBlock.test.tsx, new file):
    • Copy button click copies code and does not collapse the block (aria-expanded stays true).
    • Copy button is not a descendant of the collapsible header role="button".
    • Header click still toggles collapse.
    • Scroll container is focusable (tabIndex=0, role="region", aria-label = language or "Code").
  • Table (Table.test.tsx): scroll wrapper is focusable (tabIndex=0, role="region", aria-label="Table").
  • Markdown (Markdown.test.tsx): GFM table's outer scroll wrapper is focusable (tabIndex=0, role="region", aria-label="Table").

Verification

  • npx tsc --project packages/core/tsconfig.build.json → exit 0
  • npx eslint on all changed files → clean (exit 0)
  • npx vitest run packages/core/src/CodeBlock packages/core/src/Table packages/core/src/Markdown → 23 files, 561 tests passed
  • pnpm check:repo → clean (sync, package boundaries, changesets, demo media)

@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jul 2, 2026 4:08am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 2, 2026
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

No new or modified components detected.

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.6KB 0B

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@cixzhang cixzhang merged commit 77996c0 into main Jul 2, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants