Skip to content

feat: announce fenced-code copy result to screen readers#208

Merged
mtskf merged 5 commits into
mainfrom
feat/a11y-copy-feedback-live-region
Jul 12, 2026
Merged

feat: announce fenced-code copy result to screen readers#208
mtskf merged 5 commits into
mainfrom
feat/a11y-copy-feedback-live-region

Conversation

@mtskf

@mtskf mtskf commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

Screen readers now announce the result of the fenced-code copy button. Previously the button conveyed success/failure only by rewriting its own aria-label, and a label change on an unfocused element is not announced — so SR users who clicked (or pressed Enter on) the button got no copy confirmation. This was the only async-feedback surface in the webview without a live region.

Changes

  • fenced-code-copy-button-widget.ts: toDOM now returns a small wrapper hosting the button plus a visually-hidden aria-live status node (a sibling, not a child — a role=button subtree is often exposed atomically, so a nested live region may never be observed; mirrors the standalone role="alert" banner pattern). The copy handler announces "Copied" (polite) / "Copy failed" (assertive) alongside the existing label swap, and clears the region after the feedback window so a repeat copy re-announces.
  • theme.ts: adds the canonical sr-only clip rule for .quoll-copy-status (out of flow, so it never disturbs the collapsed open-fence row's zero height).
  • Tests: assert the region text updates on copy success and failure (with the correct politeness), is empty at build time, and clears after the feedback window; plus a theme-spec test for the sr-only clip. Existing direct-toDOM tests updated to pull the button back out of the wrapper.
  • Version bump 0.1.43 → 0.1.44 + CHANGELOG entry.

Related

  • TODO: a11y M1 — screen-reader announcement for copy-code feedback (a11y audit 2026-07-12, severity Medium)

Test Plan

  • pnpm compile green
  • pnpm test:unit green (full suite, 3332 tests)
  • pnpm lint clean on touched files
  • Manual SR smoke (VoiceOver/NVDA): copy a code block → hear "Copied"; deny clipboard permission → hear "Copy failed"

mtskf added 5 commits July 13, 2026 02:34
The copy button conveyed success/failure only by rewriting its own
aria-label, which is not announced on an unfocused element, so SR users
got no copy confirmation. Add a visually-hidden aria-live region (sibling
of the button, not nested — a role=button subtree is often exposed
atomically) that announces Copied politely and Copy failed assertively,
cleared after the feedback window so a repeat copy re-announces.
…ontracts

Address 4 MEDIUM review findings on the fenced-code copy button's SR live
region:

- Rapid-repeat re-announce: clear the live region synchronously at click
  start so a repeated identical copy result is a fresh empty->text mutation
  the AT re-announces (the 1500ms revert stays as a fallback clear). Pin it
  with a re-announce test plus a stale-revert-does-not-blank variant.
- announce() politeness-before-text ordering is now covered: spy the failure
  click path and assert aria-live is set to assertive BEFORE the text lands.
- aria-atomic="true" is asserted through the whole announce -> revert cycle,
  not only at build time.
- .quoll-copy-button-wrap gets an explicit position:static theme rule (with a
  test) so the wrapper can never silently become the absolute button's anchor.
Simplify pass: the sr-only live-region comment and the wrapper
position:static comment were mis-placed (and the latter duplicated) above
.quoll-copy-status; move each above the rule it documents. Comments only.
- Fix inaccurate 'LATER macrotask' comment on the click-start live-region
  clear: the clipboard promise settle is a microtask continuation, not a
  macrotask. Reworded to describe the async behaviour without naming a
  specific task category.
- Add a test covering the compound outcome sequence (fail via
  mockRejectedValueOnce, then succeed via mockResolvedValueOnce on an
  immediate re-click before the revert): pins that the click-start clear
  resets aria-live from assertive back to polite and clears the text
  synchronously before the second settle lands.
The compound-sequence test checked textContent==='' at the click-start
clear but not that aria-live was synchronously reset to polite, so a
regression leaving the prior failure's assertive politeness in the success
window would slip through (the later success settle re-sets polite before
the final assertion). Add the synchronous aria-live==='polite' assertion the
test's own comment already promised.
@mtskf mtskf force-pushed the feat/a11y-copy-feedback-live-region branch from d69dd52 to e550505 Compare July 12, 2026 16:35
@mtskf mtskf merged commit 09269b2 into main Jul 12, 2026
1 check passed
@mtskf mtskf deleted the feat/a11y-copy-feedback-live-region branch July 12, 2026 16:39
mtskf added a commit that referenced this pull request Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant