Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Dialog } from '@base-ui/react/dialog';
import { useDisclosure, useFullscreen } from '@mantine/hooks';
import { useDisclosure } from '@mantine/hooks';
import { memo } from 'react';
import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
import { LuCoffee } from 'react-icons/lu';
import { useLocation } from 'react-router';

import { isLocalhost, supportsFullscreen } from '../../../externals';
import { isLocalhost } from '../../../externals';
import { canUseWakeLock, useKeepAwakeOptions } from '../../../features/keep-awake/useWakeLock';
import { navigatorConstants } from '../../../viewerConfig';
import { useFullscreen } from '../../hooks/useFullscreen';
import { useIsSmallScreen } from '../../hooks/useIsSmallScreen';
import { useClientStore } from '../../stores/clientStore';
import { useViewOptionsStore } from '../../stores/viewOptions';
Expand All @@ -32,7 +33,7 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
const isSmallScreen = useIsSmallScreen();

const [isRenameOpen, handlers] = useDisclosure(false);
const { fullscreen, toggle } = useFullscreen();
const { fullscreen, isSupported: isFullscreenSupported, toggle: toggleFullscreen } = useFullscreen();
const { mirror, toggleMirror } = useViewOptionsStore();
const { keepAwake, toggleKeepAwake } = useKeepAwakeOptions();
const location = useLocation();
Expand All @@ -57,8 +58,14 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
</IconButton>
</div>
<div className={style.body}>
{supportsFullscreen && (
<NavigationMenuItem active={fullscreen} onClick={toggle}>
{isFullscreenSupported && (
<NavigationMenuItem
active={fullscreen}
onClick={() => {
onClose();
toggleFullscreen();
}}
>
Toggle Fullscreen
{fullscreen ? <IoContract /> : <IoExpand />}
</NavigationMenuItem>
Expand Down
60 changes: 60 additions & 0 deletions apps/client/src/common/hooks/useFullscreen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useCallback, useEffect, useState } from 'react';

interface UseFullscreenReturn {
/** Whether the document is currently in fullscreen mode */
fullscreen: boolean;
/**
* Whether the Fullscreen API is available and permitted in the current browsing context.
* When false, `toggle` is a no-op and the UI should hide or disable the action.
*/
isSupported: boolean;
/**
* Toggles fullscreen on/off.
* Safe to use directly as an onClick handler — the underlying Promise is handled
* internally, so rejections (eg. browser policy, user denial) never surface as
* unhandled promise rejections.
*/
toggle: () => void;
}

/**
* Manages browser fullscreen state.
*
* Compared to @mantine/hooks useFullscreen:
* - `isSupported` is co-located so there is a single source of truth
* - `toggle` is a plain void callback, safe to pass directly to event handlers
* - Error handling is internal; callers do not need try/catch
* - Only targets document.documentElement (the ref API is not used in this project)
* - No vendor-prefix shims (all supported browsers implement the standard API)
*/
export function useFullscreen(): UseFullscreenReturn {
const isSupported = Boolean(document.fullscreenEnabled);
const [fullscreen, setFullscreen] = useState(Boolean(document.fullscreenElement));

// Keep `fullscreen` state in sync with the actual browser state.
// The fullscreenchange event fires whenever the document enters or exits fullscreen
// (including when the user presses Escape to exit), so React state never drifts
// from reality. The listener is removed on unmount via the cleanup return.
useEffect(() => {
const handleChange = () => setFullscreen(Boolean(document.fullscreenElement));
document.addEventListener('fullscreenchange', handleChange);
return () => document.removeEventListener('fullscreenchange', handleChange);
}, []);

const toggle = useCallback(() => {
if (!isSupported) return;

if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {
// requestFullscreen() can be rejected in certain browser contexts
// (eg. browser security policy, extensions blocking fullscreen)
});
} else {
document.exitFullscreen().catch(() => {
// exitFullscreen() can also be rejected in rare edge cases
});
}
}, [isSupported]);

return { fullscreen, isSupported, toggle };
}
1 change: 0 additions & 1 deletion apps/client/src/externals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export const isLocalhost = currentHostName === 'localhost' || currentHostName ==
export const isOntimeCloud = document.querySelector('base')?.hasAttribute('data-is-cloud');

export const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
export const supportsFullscreen = document.fullscreenEnabled;

// resolve entrypoint URLs

Expand Down
Loading