From b06c37affaddaa9364d1be5b73a725544ad12aad Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 2 Aug 2026 23:28:39 +0200 Subject: [PATCH] [fix](trx-frontend-http): lift the header menus out of the header Fixed positioning escaped the clipping, but not the stacking: the header carries `z-index: 2`, which makes it a stacking context, so whatever z-index a menu inside it carries is confined below level 2. The spectrum overlays paint as high as 9600, so both dropdowns opened underneath them. Reparent each menu to the body when it opens. Leaving that subtree is the only way out of an ancestor's stacking context, and the menus are already positioned in viewport coordinates, so nothing else about them changes. The outside-click test now considers the menu as well as its wrapper, since the two are no longer nested. Verified by hit testing rather than by inspecting z-index: elementFromPoint at the open menu's centre returns the menu. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/generated/app.js | 9 +++++++-- .../trx-frontend-http/frontend/src/ui-core.ts | 11 +++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js index 30341776..85b09182 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js @@ -750,8 +750,9 @@ function elementById(id) { const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"]; function anchorMenu(button, menu) { const rect = button.getBoundingClientRect(); + if (menu.parentElement !== document.body) document.body.appendChild(menu); menu.style.position = "fixed"; - menu.style.zIndex = "200"; + menu.style.zIndex = "9700"; menu.style.top = "0px"; menu.style.bottom = "auto"; menu.style.right = "auto"; @@ -798,7 +799,11 @@ function elementById(id) { if (open) anchorMenu(button, menu); }); document.addEventListener("click", (event) => { - if (!(event.target instanceof Node) || !wrap.contains(event.target)) closeMenu(); + if (!(event.target instanceof Node)) { + closeMenu(); + return; + } + if (!wrap.contains(event.target) && !menu.contains(event.target)) closeMenu(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMenu(); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts index 45285c14..84b3fdc7 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/ui-core.ts @@ -334,8 +334,14 @@ function elementById(id: string): T { // edge. function anchorMenu(button: HTMLElement, menu: HTMLElement) { const rect = button.getBoundingClientRect(); + // Reparent to the body. Fixed positioning escapes clipping but not + // stacking: the header sets `z-index: 2`, which makes it a stacking + // context, so any z-index the menu carries is confined below every panel + // that paints above level 2 — the spectrum overlays reach 9600. Only + // leaving that subtree lets the menu sit above the page. + if (menu.parentElement !== document.body) document.body.appendChild(menu); menu.style.position = "fixed"; - menu.style.zIndex = "200"; + menu.style.zIndex = "9700"; menu.style.top = "0px"; menu.style.bottom = "auto"; // The stylesheet pins these menus to the right of their anchor; leaving @@ -386,7 +392,8 @@ function elementById(id: string): T { if (open) anchorMenu(button, menu); }); document.addEventListener("click", (event) => { - if (!(event.target instanceof Node) || !wrap.contains(event.target)) closeMenu(); + if (!(event.target instanceof Node)) { closeMenu(); return; } + if (!wrap.contains(event.target) && !menu.contains(event.target)) closeMenu(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMenu(); });