created a new hook for handling fullscreen rather than using react-full-screen, hopefully that will solve the issues some users are seeing where it goes into full screnn and then immediately exits

This commit is contained in:
csawatzky 2026-05-20 10:38:46 -06:00
parent e6796b1672
commit 74025290f1
4 changed files with 125 additions and 27 deletions

22
package-lock.json generated
View file

@ -52,7 +52,6 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-emoji-render": "^2.0.1", "react-emoji-render": "^2.0.1",
"react-error-boundary": "^5.0.0", "react-error-boundary": "^5.0.0",
"react-full-screen": "^1.1.1",
"react-horizontal-scrolling-menu": "^7.1.1", "react-horizontal-scrolling-menu": "^7.1.1",
"react-infinite-scroller": "^1.2.6", "react-infinite-scroller": "^1.2.6",
"react-joyride": "^2.9.3", "react-joyride": "^2.9.3",
@ -8617,12 +8616,6 @@
"node": ">=10" "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": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -11516,21 +11509,6 @@
"is-lite": "^0.8.2" "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": { "node_modules/react-horizontal-scrolling-menu": {
"version": "7.1.8", "version": "7.1.8",
"resolved": "https://registry.npmjs.org/react-horizontal-scrolling-menu/-/react-horizontal-scrolling-menu-7.1.8.tgz", "resolved": "https://registry.npmjs.org/react-horizontal-scrolling-menu/-/react-horizontal-scrolling-menu-7.1.8.tgz",

View file

@ -64,7 +64,6 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-emoji-render": "^2.0.1", "react-emoji-render": "^2.0.1",
"react-error-boundary": "^5.0.0", "react-error-boundary": "^5.0.0",
"react-full-screen": "^1.1.1",
"react-horizontal-scrolling-menu": "^7.1.1", "react-horizontal-scrolling-menu": "^7.1.1",
"react-infinite-scroller": "^1.2.6", "react-infinite-scroller": "^1.2.6",
"react-joyride": "^2.9.3", "react-joyride": "^2.9.3",

View file

@ -36,7 +36,6 @@ import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack"; import { quack } from "protobuf-ts/quack";
import { useGlobalState, useSnackbar } from "providers"; import { useGlobalState, useSnackbar } from "providers";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { FullScreen, useFullScreenHandle } from "react-full-screen";
import moment, { Moment } from "moment"; import moment, { Moment } from "moment";
import ResponsiveDialog from "common/ResponsiveDialog"; import ResponsiveDialog from "common/ResponsiveDialog";
import { getThemeType } from "theme"; import { getThemeType } from "theme";
@ -72,6 +71,7 @@ import ButtonGroup from "common/ButtonGroup";
import ModeChangeDialog from "./conditioning/modeChangeDialog"; import ModeChangeDialog from "./conditioning/modeChangeDialog";
import CustomGrainSelector from "grain/CustomGrainSelector"; import CustomGrainSelector from "grain/CustomGrainSelector";
import BinControllerDisplay from "./BinControllerDisplay"; import BinControllerDisplay from "./BinControllerDisplay";
import { useFullScreen } from "hooks/FullScreenHandle";
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
return ({ return ({
@ -217,7 +217,7 @@ export default function BinVisualizer(props: Props) {
const isMobile = useMobile(); const isMobile = useMobile();
const classes = useStyles(); const classes = useStyles();
const theme = useTheme(); const theme = useTheme();
const fullScreenHandler = useFullScreenHandle(); const {fullScreenHandler, FullScreenWrapper} = useFullScreen()
const viewport = useViewport(); const viewport = useViewport();
const { openSnack } = useSnackbar(); const { openSnack } = useSnackbar();
const [fillPercentage, setFillPercentage] = useState<number | null>(0); const [fillPercentage, setFillPercentage] = useState<number | null>(0);
@ -1344,7 +1344,7 @@ export default function BinVisualizer(props: Props) {
return ( return (
<Box display="flex" width={1} justifyContent="flex-end"> <Box display="flex" width={1} justifyContent="flex-end">
<FullScreen handle={fullScreenHandler}> <FullScreenWrapper>
<Box <Box
position="relative" position="relative"
height={1} height={1}
@ -1441,7 +1441,7 @@ export default function BinVisualizer(props: Props) {
/> />
</Box> </Box>
</Box> </Box>
</FullScreen> </FullScreenWrapper>
</Box> </Box>
); );
}; };

View file

@ -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();
* <FullScreen handle={fullScreenHandler}> ... </FullScreen>
*
* // Add:
* import { useFullScreen } from "hooks/useFullScreen";
* const { fullScreenHandler, FullScreenWrapper } = useFullScreen();
* <FullScreenWrapper> ... </FullScreenWrapper>
*
* // 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<HTMLDivElement | null>(null);
// Timestamp of the last enter() call — used to debounce spurious exit events
const enterTimeRef = useRef<number>(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 }) => (
<div ref={containerRef} style={{ width: "100%", height: "100%" }}>
{children}
</div>
),
[]
);
return {
fullScreenHandler: { active, enter, exit },
FullScreenWrapper,
};
}