From 0764eff202ef932e02534f5956184e5f96bbf6c2 Mon Sep 17 00:00:00 2001 From: mishraprayash Date: Mon, 2 Nov 2026 22:19:14 +0545 Subject: [PATCH] Fix fullscreen overlays: no remount, stacking, shortcuts, mobile scroll - Add useFullscreenOverlay hook: Esc-to-close on window (skips events already handled via defaultPrevented) and body scroll lock that saves and restores the previous overflow value; inert while closed. - SplitPanesView: drop createPortal/mounted so toggling fullscreen keeps the same element tree (panes no longer remount and lose search, scroll, collapse and undo state). A single wrapper switches to a fixed z-[45] overlay (above header z-40, below modals/toasts z-50) with role=dialog, aria-modal and an opacity-only fade (the animate-in/zoom-in classes did not exist). Below lg the overlay scrolls naturally; on lg the panes fill the viewport 50/50 and scroll internally. - SplitPanesView "f" shortcut: ignore modifier keys (stops hijacking Cmd/Ctrl+F), key repeat, IME composition, already-handled events, focus in input/textarea/select/contenteditable, and other open aria-modal dialogs. - Modal: preventDefault on Escape so one Esc closes the modal without also exiting fullscreen. - SQL designer: use the shared hook (adds Esc + scroll lock), z-50 -> z-[45], drop h-screen (100vh overflows under mobile toolbars), make the overlay scroll below lg with explicit 70dvh/80dvh rows so the export editor is not clipped (lg layout unchanged), re-fit the diagram after toggling, and use a functional state update for the toggle. - ShortcutsModal: document F (toggle fullscreen) and Esc (exit). Co-Authored-By: Claude Opus 5.5 --- src/app/tools/sql-designer/page.tsx | 23 +++- src/components/layout/ShortcutsModal.tsx | 9 +- src/components/ui/Modal.tsx | 5 +- src/components/ui/SplitPanesView.tsx | 151 +++++++++++------------ src/lib/hooks/useFullscreenOverlay.ts | 35 ++++++ 5 files changed, 139 insertions(+), 84 deletions(-) create mode 100644 src/lib/hooks/useFullscreenOverlay.ts diff --git a/src/app/tools/sql-designer/page.tsx b/src/app/tools/sql-designer/page.tsx index 6ecf793..24a20fd 100644 --- a/src/app/tools/sql-designer/page.tsx +++ b/src/app/tools/sql-designer/page.tsx @@ -15,7 +15,8 @@ import { NodeChange, EdgeChange, ConnectionMode, - MarkerType + MarkerType, + useReactFlow } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import dynamic from 'next/dynamic'; @@ -31,6 +32,7 @@ import { CopyButton } from '@/components/ui/CopyButton'; import { Select } from '@/components/ui/Select'; import { useAppStore } from '@/lib/store/useStore'; import { defineEditorThemes } from '@/tools/editor-theme'; +import { useFullscreenOverlay } from '@/lib/hooks/useFullscreenOverlay'; import { TableNode } from './TableNode'; import { initialNodes, @@ -64,6 +66,20 @@ function FlowDesigner() { const [exportFormat, setExportFormat] = React.useState('sql'); const [isFullscreen, setIsFullscreen] = React.useState(false); const [isLoaded, setIsLoaded] = React.useState(false); + const { fitView } = useReactFlow(); + + const exitFullscreen = React.useCallback(() => setIsFullscreen(false), []); + useFullscreenOverlay(isFullscreen, exitFullscreen); + + // Re-fit the diagram once the canvas has resized after entering/exiting fullscreen + // (compares against the previous value so the initial mount, incl. Strict Mode re-runs, is skipped) + const prevFullscreenRef = React.useRef(isFullscreen); + React.useEffect(() => { + if (prevFullscreenRef.current === isFullscreen) return; + prevFullscreenRef.current = isFullscreen; + const frame = requestAnimationFrame(() => fitView({ padding: 0.2 })); + return () => cancelAnimationFrame(frame); + }, [isFullscreen, fitView]); const visualChangePending = React.useRef(true); const isTypingRef = React.useRef(false); @@ -364,7 +380,8 @@ function FlowDesigner() {
@@ -383,7 +400,7 @@ function FlowDesigner() { -
- - - {/* Split Panes */} -
-
- {renderContent(leftPane)} -
-
- {renderContent(rightPane)} + // Same element tree in both modes so toggling never remounts the panes (keeps search, scroll, undo state). + // Fullscreen sits at z-45: above the header (z-40), below modals and toasts (z-50). + return ( + +
+
+ {/* Top Bar for Toolbar & Fullscreen Toggle */} +
+
+ {renderContent(toolbarContent)} +
+
+ +
+
+ + {/* Split Panes — on lg fullscreen, strictly 50/50 filling the viewport; below lg the overlay scrolls */} +
+
+ {renderContent(leftPane)} +
+
+ {renderContent(rightPane)} +
+
-
- ); - - const wrappedContent = ( - - {innerContent} ); - - if (isFullscreen && mounted) { - return createPortal( -
- {wrappedContent} -
, - document.body - ); - } - - return wrappedContent; } diff --git a/src/lib/hooks/useFullscreenOverlay.ts b/src/lib/hooks/useFullscreenOverlay.ts new file mode 100644 index 0000000..f2e1a92 --- /dev/null +++ b/src/lib/hooks/useFullscreenOverlay.ts @@ -0,0 +1,35 @@ +import * as React from 'react'; + +/** + * Shared behaviour for in-page "fullscreen" overlays (CSS fixed overlays, not the + * browser Fullscreen API): Escape closes the overlay and body scroll is locked + * while it is open. Does nothing while closed. + * + * Escape presses already handled elsewhere (e.g. a Modal calling preventDefault) + * are ignored so a single Esc only dismisses the topmost layer. + */ +export function useFullscreenOverlay(isOpen: boolean, onClose: () => void) { + const onCloseRef = React.useRef(onClose); + + React.useEffect(() => { + onCloseRef.current = onClose; + }, [onClose]); + + React.useEffect(() => { + if (!isOpen) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key !== 'Escape' || e.defaultPrevented) return; + onCloseRef.current(); + }; + + const prevOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + window.addEventListener('keydown', handleKeyDown); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + document.body.style.overflow = prevOverflow; + }; + }, [isOpen]); +}