LearnNewsExamplesServices
Frontmatter
id19984
titleA maximized dock pane's tab overflow menu closes as it opens
stateClosed
labels
bugaicoredashboard
assigneesneo-gemini-julia
createdAtOct 22, 2026, 11:19 AM
updatedAtOct 22, 2026, 4:48 PM
githubUrlhttps://github.com/neomjs/neo/issues/19984
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtOct 22, 2026, 4:48 PM

A maximized dock pane's tab overflow menu closes as it opens

Closed Backlog/active-chunk-10 bugaicoredashboard
neo-opus-ada
neo-opus-ada commented on Oct 22, 2026, 11:19 AM

Context

Found in manual testing of apps/workstation on dev (2026-10-22), in the same pass as #19982. @tobiu's screen recording shows the sequence, and I reproduced it headless (below).

Steps, using the shipped layout's 12-tab pane:

  1. Click into the pane body, maximize the pane, and click into the body again. Focus reveals the gated header actions (lock, reload, pop-out).
  2. Click ⋯ (More tabs).

Expected: the overflow menu opens and lists the hidden tabs, as it does when the pane is docked. Observed: the menu never becomes visible. The gated actions disappear, the header re-partitions (2 → 8 visible tabs), and ⋯ jumps 72 px to the right, so a second click lands on a different control.

Measurements

Headless Chromium, 1440×900, at 8bb3574d0f (its src/, apps/ and resources/scss/ are identical to dev at 84b711878a). Probes are plain Playwright, with a focusin/focusout trail and an App-worker hook on the menu.

docked maximized
after the click focus on ⋯; menu mounted at +29 ms focus on ⋯; menu mounted at +31 ms
focus moves to the first menu item and stays moves to the first menu item at +35 ms, then focusout with relatedTarget: null and the menu node is removed at +48 ms; focus lands on <body>
menu after 1 s mounted, z-index 100, hit-test inside the menu absent
gated actions / partition unchanged withdrawn / re-partitioned (+300 ms)

App-worker stack of the hide, 4 ms after afterSetMounted(true): App.onRemoteMethod → App.setConfigs → List.afterSetHidden(true) → unmount. The only main-thread caller that matches is DomAccess#align: when getClippedRect(target) returns nothing, it calls Neo.worker.App.setConfigs({id, hidden: true}).

Replaying getClippedRect's ancestor walk for ⋯:

state ⋯ rect first clipping ancestor result
docked x 1044–1068 dock-edge-zone (relative, overflow hidden, x 12–1428) the target rect
maximized x 1288–1312 dock-split (static, overflow hidden, x 233–1188), the pane's original container EMPTY

Two theories are ruled out:

  • z-index: the docked menu paints at z-index 100, above .neo-dock-maximized (--dock-maximize-z-index, default 40). The maximized menu is never painted at all; it is unmounted.
  • Header focus does not count / the tab change hides the menu: docked, focus on ⋯ and inside the menu keeps the pane's containsFocus (the menu's parentComponent chain leads to the TabContainer). Maximized, focus is already on <body> at +50 ms while the partition is still unchanged. The re-partition follows the hide; it does not cause it.

The Problem

Neo.main.DomAccess#getClippedRect starts at the target's offsetParent and walks parentElement, intersecting the rect with every ancestor whose overflow is not visible. CSS does not clip that way. Overflow clips only content whose containing-block chain passes through the clipping box (CSS 2.1 §11.1.1). A position: fixed box has the viewport as its containing block, unless an ancestor sets a transform, filter, perspective or paint containment, so it escapes every ancestor's overflow.

A maximized pane is exactly that case. .neo-dock-maximized is position: fixed (resources/scss/src/dashboard/Container.scss), and by design the pane keeps its DOM place (Maximize's @summary: "no re-parent happens"). Its descendants therefore still sit inside the original split container, whose box no longer contains them.

The chain from there: the menu's target reads as fully clipped, align() hides the menu, focus falls to <body>, and the TabContainer's containsFocus drops. The focus-gated actions are then withheld from the DOM, the strip extent grows, the overflow re-partitions, and ⋯ moves.

Inferred, not measured: every aligned floating component whose target sits in a maximized pane outside that pane's original container box is hidden the same way (header menus, pickers, tooltips).

The Architectural Reality

  • src/main/DomAccess.mjs, getClippedRect(): the walk described above. Its only caller is align(), which sets align.target = getClippedRect(targetElement) and hides the subject when that is falsy. The aligned-components refresh, which hides the subject when the target leaves the DOM, is a separate path and is not involved.
  • src/button/Base.mjs: the overflow control's menuList is floating: true, parentComponent: me, align: {edgeAlign: 't0-b0', target: me.id}.
  • src/dashboard/dock/plugin/Maximize.mjs: presentation-only maximize, no re-parent. .neo-dock-maximized is position: fixed; z-index: var(--dock-maximize-z-index, 40).
  • Design authority: align()'s auto-hide is intended for a target that is genuinely clipped out of view. The defect is the clip computation, which disagrees with CSS containing-block semantics. Maximize's no-re-parent contract is correct and stays.

The Fix

Make getClippedRect honor containing blocks. When the walk passes a position: fixed element, nothing above it clips the target, except up to and including an ancestor that establishes a containing block for fixed descendants (transform, perspective, filter, contain: paint|layout|strict|content, will-change for those properties). From there the walk continues as before. The viewport bound stays.

Trap: the same imprecision exists for position: absolute. Non-positioned clipping ancestors between an absolutely positioned box and its containing block do not clip it either. Cover it only if it falls out of the same containing-block walk; do not widen this ticket for it.

Acceptance Criteria

  • In a maximized pane, clicking ⋯ opens the overflow menu. It stays mounted, visible and focused until a selection, Escape or an outside click dismisses it, exactly as in the docked state.
  • While that menu is open, the pane's gated actions and its header partition are unchanged.
  • getClippedRect clips along the target's containing-block chain. A target under a position: fixed ancestor is not clipped by the static overflow: hidden ancestors outside that fixed ancestor. A target clipped by its own scroll container still returns the clipped rect, or nothing when it is fully hidden. (Clarified at review: "viewport-clipped" meant clipped by the fixed ancestor's containing block, the viewport, instead of by the escaped ancestors. An explicit viewport intersection is not required.)
  • E2E: a new whitebox Neural Link spec, test/playwright/e2e/workstation/WorkstationMaximizeOverflowMenuNL.spec.mjs. With a real pointer it maximizes the 12-tab pane, focuses its body and clicks ⋯. After ≥ 300 ms it asserts the menu is mounted and hit-testable, focus is inside the menu, the gated actions are present and the partition matches the pre-click one. Selecting an item then activates that tab. It must be red on dev, where the base run shows the menu unmounted with focus on <body>, and green with the fix. A docked arm in the same spec stays green. It joins the hosted Workstation NL tier (#19655).
  • Specs of other align() consumers stay green.

Out of Scope

  • #19982's stale partition after maximize, which has a separate root cause. The new spec compares the partition before and after the click and asserts no absolute tab counts, so it does not depend on #19982.
  • Returning focus to the trigger when a focused menu is force-hidden. Today that drop to <body> turns any forced hide into a header reshuffle; worth its own look if it recurs.

Related

  • #19982: the same maximized pane, the stale overflow partition.
  • #17928: dock maximize, presentation-only, no re-parent.
  • #17951: the overflow control projected as the first header action.

Live latest-open sweep: checked latest 20 open issues at 2026-10-22T09:17:48Z; no equivalent found. Keyword sweep: "getClippedRect", "menu closes immediately", "align target clipped hidden", "maximized menu": 0 results each. A2A claim sweep: no overlapping lane-claim. MC sweep: "overflow menu closes immediately in maximized dock pane…", 6 results, no prior decision (#17928 / #18122 adjacent, not the same). Own-assignment sweep: 7 open; #19982 is adjacent (same pane, different cause), none overlapping.

Origin Session ID: 70e59a84-efd6-4e7a-9262-c5478bd314ff Retrieval Hint: "maximized dock pane menu closes on open, DomAccess getClippedRect position fixed containing block, align auto-hide"

tobiu
tobiu Oct 22, 2026, 11:22 AM

https://github.com/user-attachments/assets/b58d03ee-b44c-4d52-9f34-1d7421ebac75

  • 2026-10-22T09:26:36Z @neo-opus-ada cross-referenced by #19985
  • 2026-10-22T09:38:37Z @neo-opus-ada cross-referenced by #19986
  • 2026-10-22T09:54:06Z @neo-opus-ada cross-referenced by #19987
  • 2026-10-22T10:06:29Z @neo-opus-ada cross-referenced by #19989
  • 2026-10-22T10:18:24Z @neo-opus-ada cross-referenced by #19990
  • 2026-10-22T11:03:59Z @neo-opus-ada unassigned from @neo-opus-ada
  • 2026-10-22T11:28:26Z @neo-gemini-julia assigned to @neo-gemini-julia
  • 2026-10-22T11:55:08Z @neo-gemini-julia cross-referenced by PR #19997
  • 2026-10-22T13:00:17Z @neo-gemini-julia referenced in commit b2251fc - "fix(main): honor containing blocks in DomAccess#getClippedRect (#19984)"
  • 2026-10-22T13:14:06Z @neo-opus-ada cross-referenced by #20002
tobiu closed this issue on Oct 22, 2026, 4:48 PM
tobiu referenced in commit e860b72 - "fix(main): honor containing blocks in DomAccess#getClippedRect (#19984) (#19997)" on Oct 22, 2026, 4:48 PM