fix(dashboard): portal all floating modals + lift z-band so they stack above page overlays and tap-to-front works cross-type
Root causes of modals being overlapped + tap-to-front not working across types: - Only FloatingWindow was portaled; the dock pop-out, terminal, and New Task rendered inline in their own stacking contexts, so the shared z-counter could not order them across types. Portal all three to document.body so all four floating modals share the one root stacking context. - The floating band started at 4000, below page overlays/popovers at 10000-10001 (log viewer, workflow editor, selection popover, fullscreen overlay) which painted over the modals. Raise the shared band base to 10100. Toasts already sit at 200 (below), unaffected. - Update RightDock dock-toggle test to the current behavior (popping out closes the dock; the floating modal survives) and fix NewTaskModal keyboard-vars test to query the portaled modal from document. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import "./NewTaskModal.css";
|
||||
import { useState, useCallback, useEffect, useRef, type CSSProperties, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DEFAULT_TASK_PRIORITY, type Task, type TaskCreateInput, type TaskPriority } from "@fusion/core";
|
||||
import { getErrorMessage } from "@fusion/core";
|
||||
@@ -746,7 +747,8 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
|
||||
? { left: `${position.x}px`, top: `${position.y}px`, width: `${size.width}px`, height: `${size.height}px`, zIndex }
|
||||
: keyboardStyle;
|
||||
|
||||
return (
|
||||
// FNXC:FloatingWindow 2026-06-22-22:30: Portaled to document.body so the floating New Task dialog shares the ONE root stacking context with the other floating modals; the shared cross-type z stack only orders correctly at the document root. Mobile sheet is position:fixed, unaffected.
|
||||
return createPortal(
|
||||
<div
|
||||
className="modal-overlay open new-task-modal-overlay"
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -868,6 +870,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent, type RefObject } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Maximize2, X } from "lucide-react";
|
||||
import { findOverflowViewEntry, type OverflowViewEntry, type OverflowViewKey, type OverflowViewRenderProps, type OverflowViewVisibilityOptions } from "./overflowViewRegistry";
|
||||
import { nextFloatingZ, currentFloatingZ } from "./floatingWindowStack";
|
||||
@@ -303,7 +304,8 @@ export function RightDockExpandModal({
|
||||
zIndex,
|
||||
} as CSSProperties;
|
||||
|
||||
return (
|
||||
// FNXC:FloatingWindow 2026-06-22-22:30: Portaled to document.body so this floating modal shares the ONE root stacking context with the other floating modals (FloatingWindow/terminal/New Task) — the shared 10100+ z stack only orders correctly across types when they all live at the document root.
|
||||
return createPortal(
|
||||
<div className="modal-overlay open right-dock-expand-modal-overlay" role="dialog" aria-modal="false" aria-label={`${entry.label} expanded`} data-testid="right-dock-expand-modal">
|
||||
<div
|
||||
className="modal right-dock-expand-modal right-dock-expand-modal--floating"
|
||||
@@ -343,6 +345,7 @@ export function RightDockExpandModal({
|
||||
{entry.render({ ...renderProps, surface: "expand" })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "./TerminalModal.css";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
@@ -1823,7 +1824,8 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
: {}),
|
||||
} as CSSProperties;
|
||||
|
||||
return (
|
||||
// FNXC:FloatingWindow 2026-06-22-22:30: Portaled to document.body so the terminal shares the ONE root stacking context with the other floating modals; the shared cross-type z stack only orders correctly when all panels live at the document root. Docked/floating/mobile are all position:fixed, so portaling does not change their placement.
|
||||
return createPortal(
|
||||
<div
|
||||
className={overlayClassName}
|
||||
onMouseDown={handleOverlayMouseDown}
|
||||
@@ -2295,6 +2297,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,8 +113,9 @@ describe("NewTaskModal", () => {
|
||||
viewportOffsetTop: 50,
|
||||
});
|
||||
|
||||
const { container } = renderNewTaskModal();
|
||||
const modal = container.querySelector(".new-task-modal");
|
||||
renderNewTaskModal();
|
||||
// FNXC: NewTaskModal portals to document.body, so query the modal from document (not the render container).
|
||||
const modal = document.querySelector(".new-task-modal");
|
||||
|
||||
expect(mockUseMobileKeyboard).toHaveBeenCalledWith({ enabled: true });
|
||||
expect(modal?.getAttribute("style")).toContain("--keyboard-overlap: 250px");
|
||||
|
||||
@@ -347,16 +347,18 @@ describe("RightDock", () => {
|
||||
|
||||
render(<Harness />);
|
||||
|
||||
// Pop out the currently selected (Files) view from the open dock.
|
||||
// Pop out the currently selected (Files) view: the floating modal appears AND
|
||||
// popping out closes the dock (pop-out dismisses the dock so the full-width app
|
||||
// sits behind the movable modal). The dock unmounts; the floating modal survives.
|
||||
fireEvent.click(screen.getByTestId("right-dock-expand"));
|
||||
expect(screen.getByTestId("right-dock-expand-modal")).toBeInTheDocument();
|
||||
|
||||
// Toggle the dock closed: the dock itself unmounts, the floating modal MUST survive.
|
||||
fireEvent.click(screen.getByTestId("harness-toggle-dock"));
|
||||
expect(screen.queryByTestId("right-dock")).toBeNull();
|
||||
expect(screen.getByTestId("right-dock-expand-modal")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("right-dock-expand-body")).toBeInTheDocument();
|
||||
|
||||
// Re-opening the dock does not disturb the independent floating modal.
|
||||
fireEvent.click(screen.getByTestId("harness-toggle-dock"));
|
||||
expect(screen.getByTestId("right-dock-expand-modal")).toBeInTheDocument();
|
||||
|
||||
// Its own close button still dismisses it.
|
||||
fireEvent.click(screen.getByTestId("right-dock-expand-close"));
|
||||
expect(screen.queryByTestId("right-dock-expand-modal")).toBeNull();
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
FNXC:FloatingWindow 2026-06-22-21:30:
|
||||
SHARED floating-window z-index stack. This is the ONE source of z-index for every floating modal in the dashboard (FloatingWindow, the right-dock pop-out, the floating terminal, the floating New Task dialog) so they interoperate in a SINGLE stack instead of each type owning a private counter. Previously each modal type managed z-index independently, so tapping e.g. the terminal could not raise it above a popped-out task-detail FloatingWindow. Now every floating modal claims `nextFloatingZ()` on mount/open and again on every panel pointerdown/focus, so the most-recently-interacted window is always on top REGARDLESS of type.
|
||||
|
||||
Base band sits at 4000+ — above ordinary page content and above the base `.modal-overlay` (z-index 100). The counter is module-level and intentionally monotonic: it only ever climbs, which is fine for a session-length dashboard. All floating overlays are `pointer-events: none` (click-through) so raising panels into this shared band never traps clicks on the page behind them.
|
||||
FNXC:FloatingWindow 2026-06-22-22:30:
|
||||
Base band sits at 10100+ — ABOVE the page overlay/popover band (log viewer, workflow-editor modal, selection popover, fullscreen overlay at z 10000-10001) so a floating window the user is dragging is never painted over by those. Transient top-right toasts are bumped to 10500 (styles.css) so system feedback still shows above a dragged window. The counter is module-level and intentionally monotonic: it only ever climbs, which is fine for a session-length dashboard. All floating overlays are `pointer-events: none` (click-through) so raising panels into this shared band never traps clicks on the page behind them. CRITICAL: every floating modal must be portaled to document.body so this shared z is compared in ONE root stacking context (an inline panel cannot beat siblings outside its own context no matter its z).
|
||||
*/
|
||||
let topZ = 4000;
|
||||
let topZ = 10100;
|
||||
|
||||
/** Claim the front of the shared floating-window stack. Monotonic, session-length. */
|
||||
export function nextFloatingZ(): number {
|
||||
|
||||
Reference in New Issue
Block a user