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:
Click into the pane body, maximize the pane, and click into the body again. Focus reveals the gated header actions (lock, reload, pop-out).
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"
Context
Found in manual testing of
apps/workstationondev(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:
⋯(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(itssrc/,apps/andresources/scss/are identical todevat84b711878a). Probes are plain Playwright, with afocusin/focusouttrail and an App-worker hook on the menu.⋯; menu mounted at +29 ms⋯; menu mounted at +31 msfocusoutwithrelatedTarget: nulland the menu node is removed at +48 ms; focus lands on<body>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 isDomAccess#align: whengetClippedRect(target)returns nothing, it callsNeo.worker.App.setConfigs({id, hidden: true}).Replaying
getClippedRect's ancestor walk for⋯:⋯rectdock-edge-zone(relative, overflow hidden, x 12–1428)dock-split(static, overflow hidden, x 233–1188), the pane's original containerTwo theories are ruled out:
.neo-dock-maximized(--dock-maximize-z-index, default 40). The maximized menu is never painted at all; it is unmounted.⋯and inside the menu keeps the pane'scontainsFocus(the menu'sparentComponentchain 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#getClippedRectstarts at the target'soffsetParentand walksparentElement, intersecting the rect with every ancestor whoseoverflowis notvisible. 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). Aposition: fixedbox 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-maximizedisposition: 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'scontainsFocusdrops. 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 isalign(), which setsalign.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'smenuListisfloating: true,parentComponent: me,align: {edgeAlign: 't0-b0', target: me.id}.src/dashboard/dock/plugin/Maximize.mjs: presentation-only maximize, no re-parent..neo-dock-maximizedisposition: fixed; z-index: var(--dock-maximize-z-index, 40).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
getClippedRecthonor containing blocks. When the walk passes aposition: fixedelement, 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-changefor 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
⋯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.getClippedRectclips along the target's containing-block chain. A target under aposition: fixedancestor is not clipped by the staticoverflow: hiddenancestors 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.)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 ondev, 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).align()consumers stay green.Out of Scope
<body>turns any forced hide into a header reshuffle; worth its own look if it recurs.Related
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"