useElementFullscreen.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. import { useCallback, useEffect, useRef, useState, type RefObject } from 'react';
  2. // True where the unprefixed Fullscreen API can be used on an arbitrary
  3. // element. iPhone Safari only offers it for <video>, and an <iframe> without
  4. // allowfullscreen reports fullscreenEnabled === false.
  5. export function fullscreenApiAvailable(): boolean {
  6. return (
  7. typeof document !== 'undefined' &&
  8. document.fullscreenEnabled === true &&
  9. typeof document.documentElement.requestFullscreen === 'function'
  10. );
  11. }
  12. /**
  13. * Fullscreen for a preview panel (#2976).
  14. *
  15. * Uses the Fullscreen API on the element behind `ref` where the browser has
  16. * it, and otherwise a viewport-filling layout driven by the same
  17. * `isFullscreen` flag, so the toggle keeps working on iPhone Safari and in
  18. * embedded views that refuse the request (a webview without the fullscreen
  19. * permission rejects it even though `fullscreenEnabled` says yes). In API
  20. * mode the flag follows `fullscreenchange`: Esc is handled by the browser and
  21. * simply shows up as a change, and the panel leaves fullscreen on unmount so
  22. * closing the modal never strands the document in fullscreen.
  23. */
  24. export function useElementFullscreen(ref: RefObject<HTMLElement | null>) {
  25. const apiAvailable = fullscreenApiAvailable();
  26. const [isFullscreen, setIsFullscreen] = useState(false);
  27. // True while the viewport-filling fallback is what the user sees.
  28. const fallbackActiveRef = useRef(false);
  29. useEffect(() => {
  30. if (!apiAvailable) return;
  31. const element = ref.current;
  32. const sync = () => {
  33. if (fallbackActiveRef.current) return;
  34. setIsFullscreen(document.fullscreenElement != null && document.fullscreenElement === ref.current);
  35. };
  36. document.addEventListener('fullscreenchange', sync);
  37. return () => {
  38. document.removeEventListener('fullscreenchange', sync);
  39. if (element && document.fullscreenElement === element) {
  40. Promise.resolve(document.exitFullscreen()).catch(() => {});
  41. }
  42. };
  43. }, [apiAvailable, ref]);
  44. const toggleFullscreen = useCallback(() => {
  45. if (fallbackActiveRef.current) {
  46. fallbackActiveRef.current = false;
  47. setIsFullscreen(false);
  48. return;
  49. }
  50. if (apiAvailable && document.fullscreenElement) {
  51. Promise.resolve(document.exitFullscreen()).catch(() => {});
  52. return;
  53. }
  54. const element = ref.current;
  55. if (!element) return;
  56. const enterFallback = () => {
  57. fallbackActiveRef.current = true;
  58. setIsFullscreen(true);
  59. };
  60. if (!apiAvailable) {
  61. enterFallback();
  62. return;
  63. }
  64. Promise.resolve(element.requestFullscreen()).catch(enterFallback);
  65. }, [apiAvailable, ref]);
  66. return { isFullscreen, toggleFullscreen, apiAvailable };
  67. }