Skip to content

feat(dockview-core): L4 focus management#1321

Merged
mathuo merged 5 commits into
masterfrom
feat/a11y-focus-management
Jun 11, 2026
Merged

feat(dockview-core): L4 focus management#1321
mathuo merged 5 commits into
masterfrom
feat/a11y-focus-management

Conversation

@mathuo

@mathuo mathuo commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Layer-4 focus management for the keyboard accessibility module, building on #1319 / #1320. All gated on keyboardNavigation.

1. Focus restore on close

When a panel/group holding focus is closed, focus would drop to <body> — outside the dock — silently breaking keyboard operation. The service snapshots focus on the 'remove' mutation and, after teardown, returns it to the neighbour the layout just auto-activated. Only acts when focus was actually pulled out.

2. Maximize/restore (already met — guarded)

Maximize hides siblings via visibility toggling and leaves the maximized group's DOM in place, so the active panel keeps focus by construction. Regression test guards against a future re-render-on-maximize.

3. Floating Esc returns focus

Esc inside a floating group returns focus to the invoking control (last element focused in the main dock, tracked via focusin), falling back to a grid group. Polite: bubble phase + bails on defaultPrevented, so panel content keeps Esc.

4. Floating Tab-containment

Tab wraps within a floating group (last → first, first → last on Shift+Tab) so focus doesn't leak to the grid behind the non-modal float.

Tests

1096 passing (+13 across the four pieces).

Deferred

Popout cross-window focus is intentionally out of scope here — it needs new infrastructure (a service-accessible popout-window/open event, per-popout-document listener attachment, cross-document gating) and real multi-window verification. Tracked as a dedicated follow-up.

The floating-Esc/Tab flows passed in jsdom but warrant a quick live check before relying on them, same as keyboard docking.

🤖 Generated with Claude Code

mathuo and others added 2 commits June 11, 2026 19:54
First Layer-4 focus-management piece. When removing a panel or group pulls
focus out of the dock, return it to the neighbour the layout just activated
instead of stranding it on <body> (which would silently break keyboard
operation until the user clicks back in).

The service snapshots whether focus was inside the dock on the 'remove'
mutation (focus still on the closing element), and after the teardown — if it
was inside and has now been lost — focuses the active group's content. Only
acts when focus was actually pulled out, so closing a background tab while
focused elsewhere never yanks focus into the dock. Gated on keyboardNavigation.

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

Maximize hides sibling groups via visibility toggling and leaves the maximized
group's DOM in place, so the active panel keeps focus across maximize/restore —
the L4 requirement is already met by construction. Add a regression test so a
future change that re-renders on maximize can't silently drop focus to <body>.

(The niche case of maximizing a *different* group than the focused one can lose
focus in a real browser; deferred — jsdom can't reproduce hidden-element blur,
and a fix risks stealing focus from mouse users.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mathuo mathuo changed the title feat(dockview-core): restore focus to a neighbour on close (L4 focus management) feat(dockview-core): L4 focus management — restore on close + maximize guard Jun 11, 2026
When focus is inside a floating group, Esc returns it to the invoking control —
the last element focused in the main dock before entering the float (tracked via
a focusin observer), falling back to a grid group's content if that element is
gone. Never strands focus in a dismissed float.

Deliberately polite: the handler runs in the BUBBLE phase and bails on
defaultPrevented, so panel content that uses Esc (closing a menu, clearing a
field) keeps priority. Scoped to floats inside this dock and gated on
keyboardNavigation. Float Tab-containment is a separate, later piece.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mathuo mathuo changed the title feat(dockview-core): L4 focus management — restore on close + maximize guard feat(dockview-core): L4 focus management Jun 11, 2026
mathuo and others added 2 commits June 11, 2026 20:43
Tab inside a floating group now wraps within it — at the last tabbable Tab
moves to the first, at the first Shift+Tab moves to the last — so keyboard
focus doesn't leak into the grid behind the (non-modal) float. Tabbables are
the float's naturally-focusable controls plus the active tab's roving anchor;
tabindex="-1" plumbing (content containers, inactive tabs) is excluded. Scoped
to floats in this dock and gated on keyboardNavigation.

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

The boundary-only trap leaked in a real browser: it fired only when focus was
on the exact last/first tabbable, but focus usually sits on the content
container (tabindex="-1"), which matched no tabbable — so the default Tab
escaped to the grid behind the float. (Unit tests missed it by focusing a known
button.)

Now Tab inside a float is always handled: drive the cursor through the float's
tabbables ourselves (wrapping, and snapping to first/last when focus is on
non-tabbable plumbing) and swallow the default so it can never leak out. Adds a
regression test that Tabs from the content container.

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

Copy link
Copy Markdown

@mathuo mathuo merged commit afdfa96 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