Skip to content

feat(dockview-core): LiveRegion announcer + keyboard docking (AccessibilityModule)#1319

Merged
mathuo merged 10 commits into
masterfrom
feat/live-region-announcer
Jun 11, 2026
Merged

feat(dockview-core): LiveRegion announcer + keyboard docking (AccessibilityModule)#1319
mathuo merged 10 commits into
masterfrom
feat/live-region-announcer

Conversation

@mathuo

@mathuo mathuo commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Description

Two modules that together let screen-reader and keyboard users perceive and operate the dock without a mouse. Kept on one branch as requested (they're coupled: docking narrates through the announcer).

1. LiveRegionModule

A visually-hidden aria-live="polite" status region that narrates layout changes — meeting WCAG 4.1.3 Status Messages (AA).

  • Open / close announcements (id fallback when untitled); the announce() sink.
  • Bulk-load suppressionfromJSON / clear fire one 'load'/'clear' transaction, so nested per-panel events aren't each announced (reuses the onWill/onDidMutateLayout boundary, feat(dockview-core): layout-mutation transaction events (onWillMutateLayout / onDidMutateLayout) #1317).
  • announcements: false — opt-out (e.g. when the app has its own announcer). Honoured live via updateOptions.
  • getAnnouncement(event) — localise / override / suppress the default English strings. Core ships no message catalog — just defaults + this hook — so i18n lives in the app.

2. AccessibilityModule — keyboard docking, first vertical

  • Ctrl/Cmd+M on the active panel enters move mode.
  • Arrows cycle the target group, with a live drop preview (showPreviewOverlay, the same overlay a mouse drag shows) + narration (LiveRegion.announce()).
  • Enter docks (tab-into); Escape cancels.
  • Opt-in via keyboardDocking (default off, while it matures). Keydown runs in capture phase so move-mode arrows beat the free tab-strip nav.
  • Later phases: splits (edge targets), float/popout terminals, spatial F6 nav, cross-window focus.

This is the first feature exercising the whole a11y stack end to end: showPreviewOverlay (#1318) + LiveRegion.announce() + moveGroupOrPanel.

Type of change

  • New feature (free announcer + keyboard docking)

Affected packages

  • dockview-core

How to test

  • liveRegion.spec.ts — region/roles, open/close, id fallback, the announce() sink, bulk fromJSON/clear silent, announcements:false (+ live updateOptions), getAnnouncement localise/suppress.
  • accessibilityDocking.spec.ts — Ctrl+M → Enter docks the active panel (groups 2→1) with narration; Escape cancels unchanged; inert when keyboardDocking is off.

Checklist

  • yarn test passes (full dockview-core suite: 1074)
  • prettier-clean + eslint-clean; typecheck clean
  • npm run gen run — adds the LiveRegionEvent export (the module services stay internal)
  • tests added
  • No breaking changes (announcer is SR-only/invisible and opt-out-able; keyboard docking is opt-in)

🤖 Generated with Claude Code

mathuo and others added 4 commits June 10, 2026 22:27
…f layout changes (free)

Add LiveRegionModule: a visually-hidden aria-live="polite" status region that
narrates layout state changes to assistive technology (WCAG 4.1.3 Status
Messages). Free / core, auto-registered.

Phase 1:
- the region (created at construction, clip-hidden but kept in the a11y tree)
- panel open / close announcements ("Orders opened" / "Orders closed",
  falling back to the panel id when untitled)
- the announce(message) sink — the shared region the pro accessibility module
  will write keyboard-docking narration to (one region, no drift)
- bulk-load suppression: a fromJSON / clear fires one 'load'/'clear'
  transaction, so its nested per-panel events are not each announced — reusing
  the onWill/onDidMutateLayout boundary

Float/popout/maximize announcements, active-change, the getAnnouncement /
announcer customisation hooks, and per-popout-window regions are later phases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nnouncer

New `announcements?: boolean` option (default on). Set `false` to disable the
built-in screen-reader announcements — e.g. when the host app provides its own
announcement system and would otherwise get double announcements. Read live in
announce(), so `updateOptions({ announcements: false })` takes effect
immediately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uncements

The default announcement strings are English; non-English apps need to
translate them. Add a `getAnnouncement(event)` option: return a localised
string to use it, `null` / `''` to suppress that announcement, or `undefined`
to keep the default. Core ships no message catalog — just the default strings
plus this override hook — so i18n lives in the app, not the library.

Adds the `LiveRegionEvent` type ({ kind: 'open' | 'close'; panel }).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tical)

Add the pro AccessibilityModule (dependsOn AdvancedDnD + LiveRegion). First
vertical: keyboard docking by tab-into.

- `Ctrl`/`Cmd`+`M` on the active panel enters move mode
- arrows cycle the target group, with a live drop preview
  (advancedDnDService.showPreviewOverlay — the same overlay a mouse drag shows)
  and screen-reader narration (liveRegionService.announce)
- `Enter` docks the panel into the target; `Escape` cancels
- opt-in via the new `keyboardDocking` option (default off)

The keydown handler runs in capture phase so move-mode arrows take precedence
over the free tab-strip navigation. The host (component) mediates access to the
sibling services so the service stays decoupled. Splits (edge targets),
float/popout terminals, spatial F6 nav and cross-window focus are later phases.

This is the first feature to exercise the full a11y stack end to end:
showPreviewOverlay (Phase 4) + LiveRegion announce() + moveGroupOrPanel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mathuo mathuo changed the title feat(dockview-core): LiveRegion announcer — screen-reader narration of layout changes feat(dockview-core): LiveRegion announcer + keyboard docking (AccessibilityModule) Jun 10, 2026
mathuo and others added 6 commits June 10, 2026 22:55
Cmd+M is the macOS "minimise window" shortcut, handled by the OS before the
page can intercept it (preventDefault can't stop it) — so binding it made
Ctrl/Cmd+M minimise Chrome instead of entering move mode. Use Ctrl+M only
(the spec's default), and explicitly exclude metaKey. A rebindable keymap is
the proper long-term fix and a planned follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dge), incl. splits

The first cut only docked the active panel into *another* group (tab-into) and
excluded the panel's own group — so with a single group of tabs there was no
target, move mode silently never entered, and arrows / overlays did nothing.

Rework as a two-phase move:
- PICK TARGET — arrows cycle ALL groups (including the panel's own, so a tab
  can be split out); Enter selects one.
- PICK EDGE — arrows pick a split edge (left/right/top/bottom) or centre
  (tab-into, Space/C); a live preview tracks each; Enter commits, Escape steps
  back; Escape from the target phase cancels.

This makes it work for the common single-group layout (split a tab to a side)
and adds edge/split docking, not just merge-into-existing-group.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…to the dockview

Keydowns from edge groups never reached the handler: the listener was on the
inner gridview (this.element), but edge groups live in the outer dv-shell
wrapper *outside* it — and the shell is created after this service, so a fixed
element can't be used. Listen on the document in capture phase instead, scoped
to events inside this dockview via a new host.rootElement (the shell once it
exists, else the gridview). Verified against the live demo: Ctrl+M now shows
the preview overlay and arrows drive the two-phase move.

Also guard _commit so an invalid move (e.g. splitting an edge group's only
panel) announces "that move is not allowed" instead of throwing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalise the keyboard-docking option into `keyboardNavigation` (still
opt-in), backed by a rebindable `DockviewKeybindings` keymap, and add
within-group tab switching:

- `Ctrl+]` / `Ctrl+[` cycle the focused group's tabs (wrapping round) via the
  group model's moveToNext/moveToPrevious.
- `Ctrl+M` docking is now just another keymap entry (`dock`).

Pass `keyboardNavigation: { keymap: { ... } }` to rebind any action — the
defaults deliberately avoid Cmd-based and browser-reserved combinations so they
survive on macOS. Bindings are matched modifier-exact against KeyboardEvent.key.

Renames the unreleased `keyboardDocking` option to `keyboardNavigation`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add F6 / Shift+F6 to move focus to the next / previous group (wrapping round),
as two more entries in the rebindable keymap (focusNextGroup / focusPrevGroup).
Traversal reuses the gridview's spatial next/previous and group.focus(), so it
follows the grid order and lands real DOM focus on the target group. Floating /
popout active groups aren't in the grid, so they're skipped (a later phase
covers cross-window focus). Spatial (directional) focus is also still to come.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keyboard nav stopped working after a single use: switching a tab hides the
previously focused content (and docking re-renders the grid), so the browser
dropped focus to <body>. The keymap handler only fires for events originating
inside the dock, so with focus on <body> the next keypress was ignored until
the user clicked back in.

After each action, return DOM focus to the active group's content container
(tabIndex -1, always inside the dock) via the group model's focusContent():
tab switching and group focus do it inline, docking does it after the commit /
cancel. Group focus navigation now sets the group active + focuses its content
rather than relying on the (non-focusable) group root element.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@mathuo mathuo merged commit af21b54 into master Jun 11, 2026
9 checks passed
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