Compare commits

..
Author SHA1 Message Date
sjgandClaude Opus 5 b06c37affa [fix](trx-frontend-http): lift the header menus out of the header
CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 8m21s
CI / frontend (pull_request) Successful in 3m5s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m17s
CI / test (push) Successful in 7m36s
CI / frontend (push) Successful in 2m9s
CI / reuse (push) Successful in 3s
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-02 23:28:39 +02:00
2 changed files with 16 additions and 4 deletions
@@ -750,8 +750,9 @@ function elementById(id) {
const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"]; const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"];
function anchorMenu(button, menu) { function anchorMenu(button, menu) {
const rect = button.getBoundingClientRect(); const rect = button.getBoundingClientRect();
if (menu.parentElement !== document.body) document.body.appendChild(menu);
menu.style.position = "fixed"; menu.style.position = "fixed";
menu.style.zIndex = "200"; menu.style.zIndex = "9700";
menu.style.top = "0px"; menu.style.top = "0px";
menu.style.bottom = "auto"; menu.style.bottom = "auto";
menu.style.right = "auto"; menu.style.right = "auto";
@@ -798,7 +799,11 @@ function elementById(id) {
if (open) anchorMenu(button, menu); if (open) anchorMenu(button, menu);
}); });
document.addEventListener("click", (event) => { 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) => { document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeMenu(); if (event.key === "Escape") closeMenu();
@@ -334,8 +334,14 @@ function elementById<T extends HTMLElement>(id: string): T {
// edge. // edge.
function anchorMenu(button: HTMLElement, menu: HTMLElement) { function anchorMenu(button: HTMLElement, menu: HTMLElement) {
const rect = button.getBoundingClientRect(); 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.position = "fixed";
menu.style.zIndex = "200"; menu.style.zIndex = "9700";
menu.style.top = "0px"; menu.style.top = "0px";
menu.style.bottom = "auto"; menu.style.bottom = "auto";
// The stylesheet pins these menus to the right of their anchor; leaving // The stylesheet pins these menus to the right of their anchor; leaving
@@ -386,7 +392,8 @@ function elementById<T extends HTMLElement>(id: string): T {
if (open) anchorMenu(button, menu); if (open) anchorMenu(button, menu);
}); });
document.addEventListener("click", (event) => { 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(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMenu(); });