Files
backspace/packages/web/src/hooks/usePortalContainer.ts
T
Jannis Braun e0861597bc fix(web): portal overlays into fullscreenElement so they render in voice fullscreen
requestFullscreen() on the voice container puts only its descendants in the
browser's top layer; overlays portaled to document.body were rendered outside
that layer and stayed invisible — most visibly the right-click context menu on
stream tiles and voice user panels.

Add usePortalContainer() hook returning document.fullscreenElement ?? document.body
and re-rendering on fullscreenchange. Migrate every overlay reachable during a
call: ContextMenuRenderer (desktop, submenu, mobile sheet), Tooltip,
ConfirmDialog, ConnectionInfoPopover, ScreenShareSettingsPopover, and
ScreenSharePicker (which previously rendered inline at App root).
2026-04-29 23:32:14 +02:00

32 lines
1.4 KiB
TypeScript

import { useSyncExternalStore } from 'react';
// When an element enters browser fullscreen via the Fullscreen API, the user
// agent renders only that element and its descendants. Anything portaled to
// `document.body` (the conventional overlay target) is rendered outside the
// fullscreen layer and is therefore invisible while fullscreen is active.
//
// Overlays that need to remain visible across fullscreen transitions (context
// menus, tooltips, popovers, modals, the screen-share picker) must portal into
// `document.fullscreenElement` while it is non-null and into `document.body`
// otherwise. This hook provides a reactive container that reflects the current
// fullscreen state and re-renders subscribers on every `fullscreenchange`.
function subscribe(onChange: () => void): () => void {
document.addEventListener('fullscreenchange', onChange);
return () => document.removeEventListener('fullscreenchange', onChange);
}
function getSnapshot(): Element {
return document.fullscreenElement ?? document.body;
}
function getServerSnapshot(): Element {
// Vite SPA — never invoked, but useSyncExternalStore requires a server snapshot
// for type completeness. Return body so SSR-style renders never crash.
return document.body;
}
export function usePortalContainer(): Element {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}