Fullscreen reading
The viewer leaves the browser's Fullscreen API to the host application, because which element should fill the screen and what the control looks like are decisions only the surrounding page can make. This demo passes its own reader container to requestFullscreen() instead of the whole document, so the reader and its toggle are the only things left on screen.
The button reads its label back from the fullscreenchange event rather than from the call it made, which keeps it in step when a reader leaves fullscreen through Esc or the browser's own control. Nothing has to be told about the new size: the viewer measures its container with a ResizeObserver, so the page fit and the switch between the single- and double-page layouts follow the container into fullscreen and back out on their own.
Source code
1import * as ComicViewer from "@publira/comic-viewer";2import { useCallback, useRef, useSyncExternalStore } from "react";34const subscribeToFullscreenChange = (onFullscreenChange) => {5 document.addEventListener("fullscreenchange", onFullscreenChange);67 return () => {8 document.removeEventListener("fullscreenchange", onFullscreenChange);9 };10};1112// Whether the API exists never changes, so there is nothing to listen to.13const subscribeToNothing = () => () => undefined;1415const isFullscreenEnabled = () => document.fullscreenEnabled;1617// Rendering happens on the server, where there is no Fullscreen API to ask.18const getServerSnapshot = () => false;1920const useFullscreen = (targetRef) => {21 // Reading the state back from the document, rather than from the return of22 // the request, keeps the control in step however fullscreen was left.23 const isFullscreen = useSyncExternalStore(24 subscribeToFullscreenChange,25 () =>26 targetRef.current !== null &&27 document.fullscreenElement === targetRef.current,28 getServerSnapshot29 );30 const isSupported = useSyncExternalStore(31 subscribeToNothing,32 isFullscreenEnabled,33 getServerSnapshot34 );3536 const toggleFullscreen = useCallback(async () => {37 const target = targetRef.current;3839 if (target === null) {40 return;41 }4243 if (document.fullscreenElement === null) {44 await target.requestFullscreen();45 return;46 }4748 await document.exitFullscreen();49 }, [targetRef]);5051 return { isFullscreen, isSupported, toggleFullscreen };52};5354export const Reader = ({ pages }) => {55 const containerRef = useRef(null);56 const { isFullscreen, isSupported, toggleFullscreen } =57 useFullscreen(containerRef);5859 // The container is the element handed to the screen, so the control goes60 // inside it and stays reachable once the reader fills the display.61 return (62 <div className="reader" ref={containerRef}>63 <ComicViewer.Root pages={pages}>64 <ComicViewer.Viewport />65 <ComicViewer.Toolbar />66 <ComicViewer.PageNavigation />67 </ComicViewer.Root>68 <button69 disabled={!isSupported}70 onClick={() => void toggleFullscreen()}71 type="button"72 >73 {isFullscreen ? "Exit full screen" : "Enter full screen"}74 </button>75 </div>76 );77};