Skip to content

feat(tabs): full-row activation and tree semantics for the grouped rail - #519

Open
aakhter wants to merge 2 commits into
Ark0N:masterfrom
aakhter:pr/grouped-rail-tree
Open

aakhter wants to merge 2 commits into
Ark0N:masterfrom
aakhter:pr/grouped-rail-tree

Conversation

@aakhter

@aakhter aakhter commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Stacked on #517. Only the last commit (98c6c188) is new; please review that one. I'll rebase this onto master once #517 lands.

The vertical rail's grouped view (from #517) becomes a proper ARIA tree:

  • While the owner has groups, the session list is role=tree.
  • Each named group header is a level-1 treeitem that expands and collapses and owns its rows (level 2, via aria-owns).
  • Ungrouped rows, and the selected row a collapsed group keeps visible, sit at level 1.
  • There is one roving tab stop.
  • Arrow keys follow the standard tree pattern (Up/Down move, Left collapses or climbs to the header, Right expands or enters the group), Home and End jump, and Enter or Space selects a row or toggles a group.
  • Shift+F10 or the ContextMenu key opens a row's actions, since the row's own buttons leave the tab order.
  • Focus stays on the same item across background re-renders and collapses.

The header strip, the sidebar and the flat rail keep their existing tablist/tab semantics, and the flat rail's markup is unchanged byte for byte (pinned by a test).

Activation. Clicking anywhere on a row already selected it upstream, so this PR pins that in real Chromium for the strip, the flat rail and the grouped rail. It also pins that the gear, detach, close, overflow and web-tab controls only run their own action, and that hovering a row never shifts a control out from under the pointer. Two small fixes ride along:

  • The close control shows a pointer cursor like its siblings.
  • Enter or Space on a focused web tab opens it, instead of calling selectSession(undefined).

Tests. The new Chromium suite (test/tab-activation.browser.test.ts) is added to BROWSER_TEST_GLOBS, so npm test skips it; run it with npm run test:browser. The jsdom rail tests cover roles, levels, the single tab stop, the keyboard walk and the return to tablist when grouping ends. No server changes.

Trade-offs worth a look

  • In the grouped tree the row buttons are out of the tab order. Shift+F10 reaches session-row actions and the web-tab settings, but closing a web tab is not reachable from the keyboard there.
  • I used aria-owns from the header rather than a sibling role=group. It is valid ARIA, but I've only verified it in the DOM, not with a screen reader.

The vertical tab rail now reads the owner's tab layout (GET /api/tab-layout)
and draws its groups as collapsible sections. This is the first frontend
consumer of the tab-layout backend and it is read-only: nothing in the
browser writes the layout yet.

- tab-layout-browser.js (new, pure, loaded before app.js): projects the
  layout onto the live sessions and open web tabs, renders the grouped
  markup, stores collapse per device, and sequences loads newest-wins with
  a bounded retry on failure.
- app.js: loads the layout on init and on tab:layoutChanged, renders the
  grouped rail from the same per-row markup the flat rail uses, falls
  through to a full render whenever the grouping structure changes, and
  withholds drag-reorder in the grouped rail.
- Grouping is opt-in by construction. With no layout, a failed read, a
  layout without groups, or a horizontal strip, the rail renders exactly
  as before (byte-identical markup).
- Grouping is a render layer only: sessionOrder, Alt+N, Ctrl+Tab and the
  palette keep reading the server-projected order, and row badges keep
  their Alt+N slot.
- A collapsed group still shows the active row; lineage arcs to a hidden
  session anchor to its group header.
- webview-tabs.js: renderWebviewTab() extracted so a single web tab can be
  placed into its group with unchanged markup.
The grouped vertical rail is now an ARIA tree with a tree keyboard model,
and tab rows are pinned as full-row activation targets whose controls keep
their own actions and stable hit targets.

Tree semantics (grouped vertical rail only):
- #sessionTabs becomes role=tree while grouped and returns to its shipped
  role=tablist and label when grouping ends. The header strip, sidebar and
  flat rail keep role=tablist / role=tab exactly as before (the flat rail's
  markup is unchanged byte for byte).
- A named group's header is a level-1 treeitem with aria-expanded that
  aria-owns its rows' role=group (rows are level 2). Ungrouped rows and the
  row a collapsed group keeps showing are level-1 items; a collapsed header
  owns nothing, and the "Ungrouped" heading is a visual divider hidden from
  assistive tech. aria-level, aria-setsize and aria-posinset are set on every
  item, and aria-selected follows the selection without a rebuild.
- Exactly one treeitem carries tabindex=0 (roving). Controls inside rows
  leave the tab order, so Shift+F10 / ContextMenu open a row's actions
  (session action menu, web tab settings).
- Up/Down walk visible items, Home/End jump, Right expands a header or enters
  it, Left collapses a header or climbs from a row to its header, Enter/Space
  select a row or toggle a header. With the activity sort on, the walk follows
  painted order within each group; the flat list keeps its whole-list walk.
- Focus survives a full re-render by identity (a row a collapse just hid hands
  focus to its header), but a render never pulls focus into the rail.
- The group header is the treeitem itself (no nested button), still toggled by
  click through the same onclick and still the lineage proxy anchor.

Full-row activation:
- Clicking a row's status dot, mode chip, name or padding already selected it
  upstream; that is now pinned in real Chromium for the strip, the flat rail
  and the grouped rail, together with every control (gear, detach, close,
  overflow, web tab gear and close) running only its own action.
- The close control now shows a pointer like its siblings instead of the
  default arrow.
- Enter/Space on a focused web tab in the flat list opens it; it used to call
  selectSession(undefined).
- The action controls are pinned to stay under the pointer when a row is
  hovered (no reflow-on-hover moving the gear out from under a click).

New Chromium suite test/tab-activation.browser.test.ts is listed in
BROWSER_TEST_GLOBS (run with npm run test:browser).
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