reactuals
Version:
A useful package providing a collection of 50+ React hooks and utilities to simplify React development.
125 lines (124 loc) • 4.61 kB
JavaScript
import { useCallback, useRef, useState } from "react";
/**
* Default CSS styles for the Picture-in-Picture window.
*/
const DEFAULT_PIP_STYLES = `
html, body {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
overflow: hidden;
background: #000;
}
*, *::before, *::after {
box-sizing: border-box;
}
body > * {
width: 100vw !important;
height: 100vh !important;
max-width: 100vw !important;
max-height: 100vh !important;
display: flex;
align-items: center;
justify-content: center;
}
`;
/**
* React hook to open and manage a Document Picture-in-Picture window.
*
* @param options Configuration options for the PiP window.
* @returns Object with open/close functions, state, and error info.
*/
export function usePictureInPicture(options = {}) {
// State to track if PiP is active
const [isActive, setIsActive] = useState(false);
// State to track any errors
const [error, setError] = useState(null);
// Ref to store the PiP window instance
const pipWindowRef = useRef(null);
/**
* Opens a Picture-in-Picture window with the given content.
*
* @param content The HTMLElement to display in PiP.
* @param overrideStyles Optional CSS to override default styles.
*/
const openPictureInPicture = useCallback(async (content, overrideStyles // Optional override for default styles
) => {
try {
// Check for Document Picture-in-Picture API support
if (!("documentPictureInPicture" in window)) {
throw new Error("Document Picture-in-Picture API not supported");
}
// Get the API from the window object
const docPip = window.documentPictureInPicture;
// Request a new PiP window
const pipWindow = await docPip.requestWindow(options.windowFeatures);
// Clone the provided content for the PiP window
const clonedContent = content.cloneNode(true);
// Ensure cloned content fills the PiP window
clonedContent.style.width = "100vw";
clonedContent.style.height = "100vh";
clonedContent.style.maxWidth = "100vw";
clonedContent.style.maxHeight = "100vh";
clonedContent.style.display = "flex";
clonedContent.style.alignItems = "center";
clonedContent.style.justifyContent = "center";
// Clear PiP window body and append cloned content
pipWindow.document.body.innerHTML = "";
pipWindow.document.body.appendChild(clonedContent);
// Inject styles: override, default, and custom
const style = pipWindow.document.createElement("style");
style.textContent =
(overrideStyles ?? DEFAULT_PIP_STYLES) + (options.injectStyles || "");
pipWindow.document.head.appendChild(style);
// Inject custom scripts if provided
if (options.injectScripts) {
const script = pipWindow.document.createElement("script");
script.textContent = options.injectScripts;
pipWindow.document.head.appendChild(script);
}
// Store the PiP window reference
pipWindowRef.current = pipWindow;
setIsActive(true);
setError(null);
// Call onOpen callback if provided
options.onOpen?.(pipWindow);
// Handle PiP window unload (close)
const unloadHandler = () => {
setIsActive(false);
pipWindowRef.current = null;
pipWindow.removeEventListener("unload", unloadHandler);
options.onClose?.();
};
pipWindow.addEventListener("unload", unloadHandler);
}
catch (err) {
// Handle errors and update state
const error = err instanceof Error
? err
: new Error("Failed to open Picture-in-Picture");
setError(error);
setIsActive(false);
}
}, [options]);
/**
* Closes the Picture-in-Picture window if open.
*/
const closePictureInPicture = useCallback(() => {
if (pipWindowRef.current) {
pipWindowRef.current.close();
pipWindowRef.current = null;
setIsActive(false);
options.onClose?.();
}
}, [options]);
// Return API and state for the hook
return {
openPictureInPicture,
closePictureInPicture,
isActive,
error,
pipWindow: pipWindowRef.current,
};
}