From 212feb49e2f0dda2ba170569507bac74e3a9e084 Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 9 May 2026 05:31:39 -0400 Subject: [PATCH] refactor(decnet_web/MazeNET): extract useFullscreenMode Lift the four fullscreen-related side-effects off the page shell. The hook owns: 1. body class toggle so page CSS can hide its chrome 2. browser fullscreen API request/exit (failures ignored) 3. fullscreenchange listener so F11/Esc from outside our button keeps internal state in sync 4. Esc keystroke handler Returns { fullscreen, setFullscreen, toggle }. - New MazeNET/useFullscreenMode.ts - useFullscreenMode.test.ts (jsdom) covers initial toggle, body class lifecycle, Esc-to-exit, and unmount cleanup. - MazeNET.tsx loses ~30 LOC of inline state + effects. --- decnet_web/src/components/MazeNET/MazeNET.tsx | 41 +----------- .../MazeNET/useFullscreenMode.test.ts | 48 ++++++++++++++ .../components/MazeNET/useFullscreenMode.ts | 62 +++++++++++++++++++ 3 files changed, 113 insertions(+), 38 deletions(-) create mode 100644 decnet_web/src/components/MazeNET/useFullscreenMode.test.ts create mode 100644 decnet_web/src/components/MazeNET/useFullscreenMode.ts diff --git a/decnet_web/src/components/MazeNET/MazeNET.tsx b/decnet_web/src/components/MazeNET/MazeNET.tsx index af2238b7..1cb45d77 100644 --- a/decnet_web/src/components/MazeNET/MazeNET.tsx +++ b/decnet_web/src/components/MazeNET/MazeNET.tsx @@ -22,6 +22,7 @@ import { useTopologyEditor } from './useTopologyEditor'; import { useMazeInteraction, type PaletteDrag } from './useMazeInteraction'; import { useLayoutPersistor } from './useMazeLayoutStore'; import { useTopologyStream, type TopologyStreamEvent } from './useTopologyStream'; +import { useFullscreenMode } from './useFullscreenMode'; import { ARCHETYPES as DEFAULT_ARCHETYPES } from './data'; import { useToast } from '../Toasts/useToast'; import { useServiceRegistry } from '../../hooks/useServiceRegistry'; @@ -187,43 +188,7 @@ const MazeNET: React.FC = () => { const [selection, setSelection] = useState(null); const [inspectorOpen, setInspectorOpen] = useState(true); const [paletteOpen, setPaletteOpen] = useState(true); - const [fullscreen, setFullscreen] = useState(false); - - useEffect(() => { - const cls = 'maze-fullscreen'; - if (fullscreen) document.body.classList.add(cls); - else document.body.classList.remove(cls); - return () => document.body.classList.remove(cls); - }, [fullscreen]); - - // Request/exit browser fullscreen alongside the in-app chrome hide. - // Ignore failures (fullscreen requires a user gesture; the chrome-only - // mode still works if the API rejects). - useEffect(() => { - if (fullscreen && !document.fullscreenElement) { - document.documentElement.requestFullscreen?.().catch(() => {}); - } else if (!fullscreen && document.fullscreenElement) { - document.exitFullscreen?.().catch(() => {}); - } - }, [fullscreen]); - - // Sync state if the user presses F11/Esc to leave fullscreen from - // outside our button. - useEffect(() => { - const onFsChange = () => { - if (!document.fullscreenElement) setFullscreen(false); - }; - document.addEventListener('fullscreenchange', onFsChange); - return () => document.removeEventListener('fullscreenchange', onFsChange); - }, []); - - useEffect(() => { - const onKey = (e: KeyboardEvent) => { - if (e.key === 'Escape' && fullscreen) setFullscreen(false); - }; - window.addEventListener('keydown', onKey); - return () => window.removeEventListener('keydown', onKey); - }, [fullscreen]); + const { fullscreen, toggle: toggleFullscreen } = useFullscreenMode(); const [services, setServices] = useState(DEFAULT_SERVICES); const [archetypes, setArchetypes] = useState(DEFAULT_ARCHETYPES); @@ -839,7 +804,7 @@ const MazeNET: React.FC = () => {