PXI Sidebar Modal Interaction#
The PXI (Phoenix Intelligence) assistant is a floating chat sidebar that must remain accessible even when modal overlays are open. React Aria — the library backing Phoenix's modals — marks all DOM content outside the active modal as inert and dismisses on any outside interaction, which previously caused the PXI panel to disappear or become unreachable whenever a modal opened .
PR #13417 introduced the foundational fix: detecting open modals via DOM queries, portaling PXI into the active modal's subtree, and suppressing event propagation to prevent accidental modal dismissals . Two open bugs remain — unintended PXI activation when modals close (#14197) and sidebar detachment caused by the CommandPalette (#14490).
DOM-Based Modal Detection#
Modal presence is tracked entirely through DOM queries rather than React state, so detection works across any component tree depth.
ModalOverlay stamp — every Phoenix modal overlay renders with data-testid="modal-overlay", applied unconditionally in the ModalOverlay wrapper component . Each Modal instance also marks a child element with the data-modal-portal-container attribute, which becomes the portal target for PXI.
useHasOpenModal hook — queries the DOM for all [data-testid="modal-overlay"] elements and takes the last one (the topmost in a stacked-modal scenario). It returns the active modal's portal container element. This DOM-based approach means stacked modals are handled automatically — PXI always follows the topmost open modal .
Selector constants are centralized in app/src/components/core/overlay/constants.ts (added in PR #13417) .
Layer Model and Portal Rendering#
PXI components accept a layer prop with two values :
"content"— Normal rendering at page level; FAB and panel usez-index: 999."modal"— Panel is portaled via React'screatePortal()into the active modal'sdata-modal-portal-containerelement; FAB and panel usez-index: 1002.
Layout.tsx wires this together: it calls useHasOpenModal, then passes layer={hasOpenModal ? "modal" : "content"} to FloatingAgentChatPanel . When a modal opens and the user's panel is in docked (pinned) mode, the panel is temporarily forced into floating mode; the user's stored preference is restored when the modal closes .
The chain of components that thread the layer prop: Layout → FloatingAgentChatPanel → AgentChatWidget → AgentChatPanel / AgentFabPositioner / ResizableFloatingPanel .
Z-Index Layering#
Named constants in app/src/components/core/zIndex.ts define the full overlay stack :
| Constant | Value | Purpose |
|---|---|---|
NON_MODAL_FLOATING_Z_INDEX | 999 | PXI FAB/panel at page level |
MODAL_OVERLAY_Z_INDEX | 1000 | Modal backdrop |
MODAL_DIALOG_Z_INDEX | 1001 | Modal dialog content |
MODAL_FLOATING_UI_Z_INDEX | 1002 | PXI FAB/panel inside an active modal |
PORTALED_OVERLAY_Z_INDEX | 100000 | Topmost portaled overlays |
These constants were introduced in PR #13417 to replace inline values. The base Modal.tsx still defines some values inline: z-index: 100 for the slideover variant , z-index: 1001 for the default dialog , and z-index: 1000 for the overlay backdrop .
Event Propagation Suppression#
Two patterns prevent unintended modal dismissals:
PXI pointer events — ResizableFloatingPanel and AgentFabPositioner attach a stopModalLayerPropagation handler to onClick, onPointerDown, onPointerMove, onPointerUp, and onPointerCancel. When layer === "modal", each handler calls event.stopPropagation(), preventing React Aria's overlay from receiving these events and treating them as "outside" clicks that should dismiss the modal. The full pointer event set is required because drag/resize interactions generate a continuous stream of pointer events .
Escape key in CodeMirror — EditCodeEvaluatorDialogContent wraps its CodeMirror editor with an onKeyDown handler that calls event.stopPropagation() on Escape, preventing CodeMirror's edit-mode exit from also closing the enclosing slideover panel (PR #12613) .
Accessibility: Interactivity Restoration#
React Aria continuously re-applies inert and aria-hidden to elements outside the modal's focus scope — even after PXI is portaled inside the modal.
useModalFloatingLayerInteractivity (in app/src/components/agent/) counters this with a MutationObserver that removes inert and aria-hidden from PXI elements on every attribute mutation, so repeated React Aria updates cannot permanently lock out the assistant .
Known Bugs and Edge Cases#
PXI opens unprompted when modals close (Issue #14197)#
Root cause: When a modal closes, the PXI FAB transitions from the modal portal back to the page layer. The pointer event sequence from dismissing the modal (click/pointerup) can land on the now-repositioned FAB, inadvertently activating it. Repros reliably when creating or deleting an annotation config .
Fix direction: A transition guard that prevents pointer activation of the FAB during the brief re-layering window .
CommandPalette detaches PXI sidebar (Issues #14489, #14490)#
Root cause: The ⌘K CommandPalette (PR #14114) is a React Aria-based modal . An auto-focus useLayoutEffect in Chat.tsx focuses the chat textarea whenever autoFocusInput is true and specific dialogs (consent gate, elicitation, rewind) are not active — but it does not check for the command palette or other arbitrary modals. When any of its dependencies change while the palette is open, the effect re-fires and steals focus from the palette, causing the sidebar to appear detached .
Proposed fix: Add a hasOpenModal guard to the useLayoutEffect in app/src/components/agent/Chat.tsx using the existing useHasOpenModal hook, so auto-focus is suppressed whenever any modal is open .
Key Files#
| File | Purpose |
|---|---|
app/src/components/overlay/Modal.tsx | Core Modal and ModalOverlay; stamps data-testid="modal-overlay" and data-modal-portal-container |
app/src/components/core/overlay/constants.ts | DOM selector constants for modal overlay and portal container queries |
app/src/components/core/zIndex.ts | Named z-index constants for the full overlay stack |
app/src/hooks/useHasOpenModal.ts | Detects open modals; returns the active modal's portal container |
app/src/components/agent/useModalFloatingLayerInteractivity.ts | MutationObserver that removes inert/aria-hidden from PXI elements |
app/src/components/agent/ResizableFloatingPanel.tsx | PXI panel with layer-aware z-index and pointer event suppression |
app/src/components/agent/AgentFabPositioner.tsx | PXI FAB with layer-aware z-index and pointer event suppression |
app/src/pages/Layout.tsx | Top-level layout; passes layer prop to FloatingAgentChatPanel |
app/src/components/agent/Chat.tsx | Contains auto-focus useLayoutEffect (source of focus-stealing bug) |
app/src/components/core/commandpalette/CommandPalette.tsx | ⌘K palette; React Aria modal not covered by PXI's auto-focus guard |
Related PRs: #13417 — foundational modal accessibility for PXI · #14114 — CommandPalette · #12613 — Escape key suppression in CodeMirror
Open issues: #14197 — PXI opens on modal close · #14489 · #14490 — CommandPalette detaches PXI