feat(dockview-core): LiveRegion announcer + keyboard docking (AccessibilityModule)#1319
Merged
Conversation
…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>
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>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



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.
LiveRegionModuleA visually-hidden
aria-live="polite"status region that narrates layout changes — meeting WCAG 4.1.3 Status Messages (AA).announce()sink.fromJSON/clearfire one'load'/'clear'transaction, so nested per-panel events aren't each announced (reuses theonWill/onDidMutateLayoutboundary, 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 viaupdateOptions.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 verticalCtrl/Cmd+Mon the active panel enters move mode.showPreviewOverlay, the same overlay a mouse drag shows) + narration (LiveRegion.announce()).Enterdocks (tab-into);Escapecancels.keyboardDocking(default off, while it matures). Keydown runs in capture phase so move-mode arrows beat the free tab-strip nav.This is the first feature exercising the whole a11y stack end to end:
showPreviewOverlay(#1318) +LiveRegion.announce()+moveGroupOrPanel.Type of change
Affected packages
dockview-coreHow to test
liveRegion.spec.ts— region/roles, open/close, id fallback, theannounce()sink, bulkfromJSON/clearsilent,announcements:false(+ liveupdateOptions),getAnnouncementlocalise/suppress.accessibilityDocking.spec.ts— Ctrl+M → Enter docks the active panel (groups 2→1) with narration; Escape cancels unchanged; inert whenkeyboardDockingis off.Checklist
yarn testpasses (fulldockview-coresuite: 1074)npm run genrun — adds theLiveRegionEventexport (the module services stay internal)🤖 Generated with Claude Code