import { useCallback, useEffect, useRef, useState } from "react"; /** * Drop-in replacement for react-full-screen's useFullScreenHandle + FullScreen component. * * Motivation: react-full-screen 1.1.1 doesn't debounce fullscreenchange events, so on * Windows systems with DPI scaling > 100%, the resize triggered during fullscreen entry * fires a fullscreenchange event that the library misinterprets as an exit, immediately * popping back out of fullscreen. * * This hook bypasses the library entirely and calls the native Fullscreen API directly, * ignoring fullscreenchange events fired within DEBOUNCE_MS of entry to absorb the * DPI-scaling resize blip. * * Usage — replace in BinVisualizerV2.tsx: * * // Remove: * import { FullScreen, useFullScreenHandle } from "react-full-screen"; * const fullScreenHandler = useFullScreenHandle(); * ... * * // Add: * import { useFullScreen } from "hooks/useFullScreen"; * const { fullScreenHandler, FullScreenWrapper } = useFullScreen(); * ... * * // Everything else (fullScreenHandler.active, .enter(), .exit()) stays the same. */ const DEBOUNCE_MS = 500; export interface FullScreenHandle { active: boolean; enter: () => void; exit: () => void; } export function useFullScreen(): { fullScreenHandler: FullScreenHandle; FullScreenWrapper: React.FC<{ children: React.ReactNode }>; } { const [active, setActive] = useState(false); const containerRef = useRef(null); // Timestamp of the last enter() call — used to debounce spurious exit events const enterTimeRef = useRef(0); const enter = useCallback(() => { const el = containerRef.current; if (!el) return; enterTimeRef.current = Date.now(); if (el.requestFullscreen) { el.requestFullscreen().catch(() => { // Some browsers (e.g. iOS Safari) reject the promise — silently ignore }); } else if ((el as any).webkitRequestFullscreen) { (el as any).webkitRequestFullscreen(); } else if ((el as any).mozRequestFullScreen) { (el as any).mozRequestFullScreen(); } else if ((el as any).msRequestFullscreen) { (el as any).msRequestFullscreen(); } }, []); const exit = useCallback(() => { if (document.exitFullscreen) { document.exitFullscreen().catch(() => {}); } else if ((document as any).webkitExitFullscreen) { (document as any).webkitExitFullscreen(); } else if ((document as any).mozCancelFullScreen) { (document as any).mozCancelFullScreen(); } else if ((document as any).msExitFullscreen) { (document as any).msExitFullscreen(); } }, []); useEffect(() => { const handleChange = () => { const fullscreenEl = document.fullscreenElement || (document as any).webkitFullscreenElement || (document as any).mozFullScreenElement || (document as any).msFullscreenElement; const isNowFullscreen = fullscreenEl === containerRef.current; // If we just called enter() and this event fires within DEBOUNCE_MS, // and it looks like an exit, ignore it — it's the DPI-scaling resize blip if (!isNowFullscreen && Date.now() - enterTimeRef.current < DEBOUNCE_MS) { return; } setActive(isNowFullscreen); }; document.addEventListener("fullscreenchange", handleChange); document.addEventListener("webkitfullscreenchange", handleChange); document.addEventListener("mozfullscreenchange", handleChange); document.addEventListener("MSFullscreenChange", handleChange); return () => { document.removeEventListener("fullscreenchange", handleChange); document.removeEventListener("webkitfullscreenchange", handleChange); document.removeEventListener("mozfullscreenchange", handleChange); document.removeEventListener("MSFullscreenChange", handleChange); }; }, []); const FullScreenWrapper: React.FC<{ children: React.ReactNode }> = useCallback( ({ children }) => (
{children}
), [] ); return { fullScreenHandler: { active, enter, exit }, FullScreenWrapper, }; }