From 52e16e7807bb981ece5b80230292fb30f1069cd6 Mon Sep 17 00:00:00 2001 From: Claude VM Date: Wed, 20 May 2026 08:30:26 +0300 Subject: [PATCH] fix(cf-modal): portal to body + auto-close on parcel switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related issues with the modal when the user kept clicking around the map while in CF order mode: 1. LAYOUT BREAK (Marius screenshot — modal header clipped above viewport): The V2 panel wrapper uses `backdrop-blur-md`. Per CSS spec, an element with non-none backdrop-filter establishes a containing block for `fixed`-positioned descendants. So `fixed inset-0` on the modal was relative to the panel (top-right, ~50px tall at min) instead of the viewport — the modal anchored to the panel and overflowed up. Fix: render via React's createPortal to document.body. The modal now escapes the panel's stacking context entirely and centers in the viewport. Also bumped z-index from 50 to 100 so the modal stays above the MapLibre canvas + panel itself. 2. STATE CARRY-OVER: clicking a different parcel while the modal was open silently re-targeted the modal at the new parcel — same modal showing different cadref/sold mid-flow could mislead the user about which parcel they were buying CF for. Fix: FeatureInfoPanel now has a useEffect that closes the modal when feature.cadastralRef / siruta / layerId changes. Modal stays scoped to a single decision. SSR guard: if (typeof document === "undefined") return null; before the portal call so the modal doesn't blow up during server-side render. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/modules/geoportal/v2/cf-order-modal.tsx | 14 +++++++++++--- src/modules/geoportal/v2/feature-info-panel.tsx | 7 +++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/modules/geoportal/v2/cf-order-modal.tsx b/src/modules/geoportal/v2/cf-order-modal.tsx index 7ae3dc6..6b86f04 100644 --- a/src/modules/geoportal/v2/cf-order-modal.tsx +++ b/src/modules/geoportal/v2/cf-order-modal.tsx @@ -17,6 +17,7 @@ // error → any failure with retry option import { useCallback, useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; import { X, Loader2, Check, AlertCircle, FileText, Coins, Plug, ArrowRight, Download, @@ -276,12 +277,18 @@ export function CfOrderModal({ ); if (!open) return null; + // Render via portal so the modal escapes the panel's containing + // block — the parent V2 panel uses backdrop-blur-md which (per CSS + // spec) creates a containing block for `fixed` descendants. Without + // the portal the modal anchors to the panel's top-right corner and + // gets clipped above the viewport. + if (typeof document === "undefined") return null; - return ( + return createPortal(
{ // dismiss on backdrop click only when not mid-flight if ( @@ -602,7 +609,8 @@ export function CfOrderModal({ )}
- + , + document.body, ); } diff --git a/src/modules/geoportal/v2/feature-info-panel.tsx b/src/modules/geoportal/v2/feature-info-panel.tsx index 702317a..6712284 100644 --- a/src/modules/geoportal/v2/feature-info-panel.tsx +++ b/src/modules/geoportal/v2/feature-info-panel.tsx @@ -406,6 +406,13 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa const [condoLoading, setCondoLoading] = useState(false); const [cfModalOpen, setCfModalOpen] = useState(false); + // Close the CF modal whenever the user switches to a different + // parcel — keeps the modal scoped to a single decision instead of + // silently re-targeting mid-flight. + useEffect(() => { + setCfModalOpen(false); + }, [feature.cadastralRef, feature.siruta, feature.layerId]); + const authRetriedRef = useRef( typeof window !== "undefined" && sessionStorage.getItem(AUTH_RETRY_KEY) === "1",