LearnNewsExamplesServices
Frontmatter
id19293
titleThe browser arm for a focused non-active tab surviving the overflow projection: staging unresolved against the app''s real headers
stateClosed
labels
bugaicoreaccessibility
assigneesneo-gpt
createdAtSep 26, 2026, 8:57 PM
updatedAtSep 27, 2026, 4:28 PM
githubUrlhttps://github.com/neomjs/neo/issues/19293
authorneo-preview
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 19285 The tab Overflow projection withholds a focused tab and drops focus to the body
blocking[]
closedAtSep 27, 2026, 4:28 PM

The browser arm for a focused non-active tab surviving the overflow projection: staging unresolved against the app's real headers

Closed v13.2.0/archive-v13-2-0-chunk-23 bugaicoreaccessibility
neo-preview
neo-preview commented on Sep 26, 2026, 8:57 PM

Test-only remainder of #19285. The engine fix and its unit arms shipped; the observable document.activeElement outcome does not have a landed arm, and the staging is the hard part, so this ticket carries what was measured rather than leaving the next attempt to re-derive it.

What the arm must witness

A non-active tab holds focus, the header narrows until it overflows, and after the projection: the tab is still in the DOM with a real box, document.activeElement is still inside the tab container, and no action node the focused header owned before the projection was withdrawn. The last clause is the one that matters — a contextually inactive action has NO DOM node at all, so a dropped node is the observable, not a class or a style.

The fix it witnesses, and why no other shape can

The partition keeps the focused node. Handing focus to the More tabs control cannot work: syncControl creates the control with autoInitVnode + autoMount, a promise, so focus() resolves its node through DomAccess.getElement and gets null; and the plugin already observes the control mounted edge (Overflow.mjs:1245-1249), which fires a render round trip AFTER button.hidden = true removed the focused node. The actions withdrawal is therefore causal, not a race — a late hand-off restores activeElement and cannot un-withdraw the actions. An arm that only asserts activeElement is inside the container would pass a build that still drops the partition.

What was measured, and why the arm is not landed

Staged against WorkstationTabOverflowCapNL, which is the right host: same predicate discipline, same staging machinery, and a documented precedent for exactly this failure. Five staging facts, each measured:

  • The stage must be self-sufficient. Run in the full file, the arm passed its overflow poll; run alone, it failed it. The cap arm above leaves the viewport at 760, so a hard-coded setViewportSize(760) is a NO-OP — no width change, no geometry change, no pass scheduled. Compute the narrowing from the current width and assert it actually changed.
  • The focus premise must be asserted before the stage. Focusing the raw DOM node (not Neo.get, which is a worker-side namespace and absent from window) drives manager.Focus through the real event path. Without asserting document.activeElement before the projection, the arm can pass on a build where the tab never held focus and the partition was never at risk.
  • The focus-driven reveal is asynchronous, so the baseline must be read after it settles. Snapshotted immediately after the focus, the partition read 1 action; 150ms later it read 6. That is the reveal arriving, not the projection changing anything. Poll for a stable count (two equal reads) before taking the baseline, or the arm asserts a count INCREASE and convicts a correct build.
  • The partition count must be asserted as set membership, not equality. Across the narrowing it went 6 to 7 and the extra node was a DIFFERENT action the narrower header legitimately adds. The defect REMOVES nodes; an equality assertion would convict a correct build for a responsive addition. Assert that every id present before is still present after.
  • The obvious overflow staging is the wrong one. Widening some OTHER tab does not exercise the fix: with both ids protected the widened ACTIVE tab wins the displacement and the focused tab is withheld either way, so the arm is red on a correct build. Widening the FOCUSED tab is correct, because in-order packing overflows the LAST tab regardless of its width — used + width <= usable fails on the width, not on its being large — so the extra width buys no survival on its own.

Where it stopped

With all of the above applied, the selected header (neo-tab-header-toolbar-5, two tabs) still produced no overflow control inside it within the poll, despite a real narrowing and a widened label. That is a staging gap against the app CURRENT composition, not evidence about the fix — and the host spec already carries the scar of two peers spending a bisect on a staging defect that looked like a product regression. So the arm was reverted rather than landed half-staged, and the unit arms (red-first, 6 red on base / 54 green at head, 161 across tab + toolbar + DockLayoutAdapter) carry the fix in the meantime.

The open question for whoever takes it: the app non-reveal headers may need a THIRD tab to stage this at all, since a two-tab header ends with both ids protected and nothing withheld — the joint-overflow bound documented on #19285, where no control is torn down because nothing overflows.

Acceptance Criteria

  • AC-1: red-first on the base — the arm fails with activeElement on body and/or the focused node absent from the DOM.
  • AC-2: green at the head — the focused node survives with a real box, focus stays inside the tab container, and no pre-existing action node is withdrawn.
  • AC-3: the arm is self-sufficient — green run alone AND green in the full file, with the narrowing computed from the live viewport width.
  • AC-4: the stage is predicate-selected and reports rejected candidates on failure, per the host spec documented substitution lessons.

Related

#19285 (the engine fix + the unit arms), #17844 (the NL battery this arm joins), WorkstationTabOverflowCapNL.spec.mjs (the host), manager.Focus#setComponentFocus, Overflow.mjs syncControl / applySplit.

unowned-rationale: the engine half is merged under #19285 and this is its test-only remainder; Vega released #19285 to me, and the staging above is the expensive part, so it is deliberately left open rather than half-landed.

tobiu referenced in commit ff9e3d2 - "test(workstation): witness focused tab through overflow (#19293)" on Sep 26, 2026, 10:22 PM
tobiu referenced in commit a50ae57 - "fix(tab): the Overflow projection never withholds the tab that holds focus (#19285) (#19294) on Sep 27, 2026, 12:06 AM
tobiu referenced in commit 5e8d32b - "test(workstation): keep focus witness comment durable (#19293)" on Sep 27, 2026, 1:29 PM
tobiu referenced in commit 627656a - "test(workstation): witness focused tab through overflow (#19293) (#19310) on Sep 27, 2026, 4:28 PM
tobiu closed this issue on Sep 27, 2026, 4:28 PM