diff --git a/package-lock.json b/package-lock.json index a8d3068..58133fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,6 @@ "react-dom": "^18.3.1", "react-emoji-render": "^2.0.1", "react-error-boundary": "^5.0.0", - "react-full-screen": "^1.1.1", "react-horizontal-scrolling-menu": "^7.1.1", "react-infinite-scroller": "^1.2.6", "react-joyride": "^2.9.3", @@ -8617,12 +8616,6 @@ "node": ">=10" } }, - "node_modules/fscreen": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz", - "integrity": "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==", - "license": "MIT" - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -11516,21 +11509,6 @@ "is-lite": "^0.8.2" } }, - "node_modules/react-full-screen": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/react-full-screen/-/react-full-screen-1.1.1.tgz", - "integrity": "sha512-xoEgkoTiN0dw9cjYYGViiMCBYbkS97BYb4bHPhQVWXj1UnOs8PZ1rPzpX+2HMhuvQV1jA5AF9GaRbO3fA5aZtg==", - "license": "MIT", - "dependencies": { - "fscreen": "^1.0.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": ">= 16.8.0" - } - }, "node_modules/react-horizontal-scrolling-menu": { "version": "7.1.8", "resolved": "https://registry.npmjs.org/react-horizontal-scrolling-menu/-/react-horizontal-scrolling-menu-7.1.8.tgz", diff --git a/package.json b/package.json index 90ac71b..a7afb36 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "react-dom": "^18.3.1", "react-emoji-render": "^2.0.1", "react-error-boundary": "^5.0.0", - "react-full-screen": "^1.1.1", "react-horizontal-scrolling-menu": "^7.1.1", "react-infinite-scroller": "^1.2.6", "react-joyride": "^2.9.3", diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx index f6f9c1d..1f7c26d 100644 --- a/src/bin/BinVisualizerV2.tsx +++ b/src/bin/BinVisualizerV2.tsx @@ -36,7 +36,6 @@ import { pond } from "protobuf-ts/pond"; import { quack } from "protobuf-ts/quack"; import { useGlobalState, useSnackbar } from "providers"; import React, { useCallback, useEffect, useState } from "react"; -import { FullScreen, useFullScreenHandle } from "react-full-screen"; import moment, { Moment } from "moment"; import ResponsiveDialog from "common/ResponsiveDialog"; import { getThemeType } from "theme"; @@ -72,6 +71,7 @@ import ButtonGroup from "common/ButtonGroup"; import ModeChangeDialog from "./conditioning/modeChangeDialog"; import CustomGrainSelector from "grain/CustomGrainSelector"; import BinControllerDisplay from "./BinControllerDisplay"; +import { useFullScreen } from "hooks/FullScreenHandle"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -217,7 +217,7 @@ export default function BinVisualizer(props: Props) { const isMobile = useMobile(); const classes = useStyles(); const theme = useTheme(); - const fullScreenHandler = useFullScreenHandle(); + const {fullScreenHandler, FullScreenWrapper} = useFullScreen() const viewport = useViewport(); const { openSnack } = useSnackbar(); const [fillPercentage, setFillPercentage] = useState(0); @@ -1344,7 +1344,7 @@ export default function BinVisualizer(props: Props) { return ( - + - + ); }; diff --git a/src/hooks/FullScreenHandle.tsx b/src/hooks/FullScreenHandle.tsx new file mode 100644 index 0000000..9e4323b --- /dev/null +++ b/src/hooks/FullScreenHandle.tsx @@ -0,0 +1,121 @@ +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, + }; +} \ No newline at end of file