diff --git a/src/ui/panelDock.test.mjs b/src/ui/panelDock.test.mjs index ea4f8f0a9..ee9858a95 100644 --- a/src/ui/panelDock.test.mjs +++ b/src/ui/panelDock.test.mjs @@ -15,7 +15,9 @@ function fixture() { const classes = new Set(); const style = { removeProperty(name) { + // Like CSSStyleDeclaration: 'z-index' also clears style.zIndex. delete this[name]; + delete this[name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())]; }, getPropertyValue: () => '', }; @@ -191,3 +193,55 @@ test('two quick header presses snap a floating panel back; slow or distant ones f.restore(); } }); + +test('a restored floating window stacks above the docked DISPLAY panel (z 110)', () => { + const f = fixture(); + try { + const panel = f.element(); + panel.id = 'street-level-panel'; + f.owner._portablePanels.set('street-level-panel', { + panel, + min: { width: 320, height: 280 }, + dockOnCollapse: true, + }); + f.store.set( + 'godsEyeView.v8.panelPos.street-level-panel', + JSON.stringify({ + left: 900, + top: 200, + width: 420, + height: 600, + floating: true, + }), + ); + f.owner._restorePanelPosition('street-level-panel', panel); + assert.equal(panel.classList.contains('panel-floating'), true); + assert.ok(Number(panel.style.zIndex) > 110, `z ${panel.style.zIndex}`); + f.owner.dockPanel('street-level-panel'); + assert.equal(panel.style.zIndex, undefined, 'docking drops the promotion'); + } finally { + f.restore(); + } +}); + +test('renumbering the z ladder never drops a window below the docked panels', () => { + const f = fixture(); + try { + const windows = [f.element(), f.element(), f.element()]; + windows.forEach((node, index) => { + node.classList.add('panel-draggable'); + node.style.zIndex = String(130 + index); + }); + globalThis.document.querySelectorAll = (selector) => + selector === '.panel-draggable' ? windows : []; + f.owner._panelZCounter = 139; + const top = f.element(); + top.classList.add('panel-draggable'); + f.owner._promotePanelZ(top); + for (const node of [...windows, top]) + assert.ok(Number(node.style.zIndex) > 110, `z ${node.style.zIndex}`); + assert.ok(Number(top.style.zIndex) > Number(windows[2].style.zIndex)); + } finally { + f.restore(); + } +}); diff --git a/src/ui/panelPositionControls.js b/src/ui/panelPositionControls.js index 7ce0651cb..97ecf634a 100644 --- a/src/ui/panelPositionControls.js +++ b/src/ui/panelPositionControls.js @@ -12,6 +12,13 @@ const PANEL_POSITION_STORAGE_VERSION = 'v8'; /** Z ladder: panels promote within [100, 139]; voice pill 150, toast 200, clean-view-exit 300. */ const PANEL_Z_BASE = 100; const PANEL_Z_MAX = 139; +/** + * Lowest z a promoted (dragged or floating) panel may take: above every panel + * docked in a rail, the highest of which is #pp-toggles at 110 (controls.css). + * Renumbering restarts here, so a floating window never slips under DISPLAY. + * Keep in step with `#right-context-rail > .panel-floating` in controls.css. + */ +const PANEL_Z_FLOATING_FLOOR = PANEL_Z_BASE + 11; /** Pointer travel before a header press becomes a drag that lifts a portable panel out. */ const DRAG_THRESHOLD_PX = 4; /** @@ -37,6 +44,8 @@ const FLOATING_STYLE_PROPERTIES = [ 'bottom', 'width', 'height', + // A docked panel takes the rail's stacking, not a floating window's. + 'z-index', ]; function clamp(value, min, max) { @@ -264,6 +273,9 @@ export class PanelPositionControls { if (Boolean(pos.floating) !== Boolean(portable)) return; if (portable) { panelEl.classList.add('panel-floating', 'panel-draggable'); + // A restored window must stack above the docked rail panels too, not + // only one that was lifted or clicked this session. + this._promotePanelZ(panelEl); if (Number.isFinite(pos.width)) { panelEl.style.width = `${clamp( Math.round(pos.width), @@ -337,7 +349,7 @@ export class PanelPositionControls { const promoted = [...document.querySelectorAll('.panel-draggable')] .filter((el) => el.style.zIndex) .sort((a, b) => Number(a.style.zIndex) - Number(b.style.zIndex)); - let z = PANEL_Z_BASE + 1; + let z = PANEL_Z_FLOATING_FLOOR; for (const el of promoted) { el.style.zIndex = String(z); z += 1; diff --git a/src/ui/styles/controls.css b/src/ui/styles/controls.css index 98a9f10d3..323b59ef6 100644 --- a/src/ui/styles/controls.css +++ b/src/ui/styles/controls.css @@ -117,6 +117,9 @@ #right-context-rail > .panel-floating { position: fixed; inset: auto; + /* Above every docked rail panel (#pp-toggles is 110). PanelPositionControls + promotes floating windows from this floor (PANEL_Z_FLOATING_FLOOR). */ + z-index: 111; max-height: calc(100vh - 12px); margin: 0; flex: none;