// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file useOverlay.tsx * @input Overlay options (showOn, scrim, position, content, etc.) * @output containerRef, containerProps, element, renderOverlay * @position Core hook for overlay system — same pattern as useTooltip * * For applying overlay behavior to an existing container (Card, * custom elements) without a wrapper. Returns props to spread on the * container and the scrim to render inside it. * * Handles: marker, positioning, touch toggle (via useClickableContainer), * and scrim rendering. */ import { type ReactNode, type ReactElement, type MouseEvent, useState, useRef, useCallback, useMemo, useSyncExternalStore } from 'react'; import * as stylex from "../style-runtime.js"; import { useClickableContainer } from "../hooks/useClickableContainer.js"; import { overlayScope, overlayContainerStyles } from "./overlay.markers.stylex.js"; import { OverlayScrim } from "./OverlayScrim.js"; import type { OverlayScrimMode, OverlayPosition, OverlayAlign, OverlayShowOn } from "./OverlayScrim.js"; // ============================================================================= // Touch detection // ============================================================================= function getIsTouchDevice(): boolean { if (typeof window === 'undefined') { return false; } return window.matchMedia('(hover: none)').matches; } function subscribeToHoverChange(callback: () => void): () => void { if (typeof window === 'undefined') { return () => {}; } const mql = window.matchMedia('(hover: none)'); mql.addEventListener('change', callback); return () => mql.removeEventListener('change', callback); } function getServerSnapshot(): boolean { return false; } // ============================================================================= // Types // ============================================================================= export interface UseOverlayOptions { /** Content rendered inside the scrim overlay. */ content?: ReactNode; /** * CSS-driven visibility trigger. * - `"always"` — always visible * - `"hover"` — hover + focus (accessible default). Touch: strip = always visible, fill = tap-to-toggle. * - `"focus"` — focus-within only * - `"hover-or-focus"` — alias for "hover" * @default "always" */ showOn?: OverlayShowOn; /** * JS-controlled visibility override. Takes precedence over showOn + touch. */ isOpen?: boolean; /** * Scrim background mode. * @default "dark" */ scrim?: OverlayScrimMode; /** * Scrim placement. * @default "fill" */ position?: OverlayPosition; /** * Content alignment. * @default "end" */ align?: OverlayAlign; } export interface OverlayContainerProps { /** CSS class names for marker + positioning. */ className: string | undefined; /** Inline styles for marker + positioning. */ style: React.CSSProperties | undefined; /** Touch tap-to-toggle handler. Only set on touch devices with full overlays. */ onClick: ((e: MouseEvent) => void) | undefined; /** Touch tap-to-toggle handler. Only set on touch devices with full overlays. */ onMouseUp: ((e: MouseEvent) => void) | undefined; } export interface UseOverlayResult { /** Ref — attach to the container element. */ containerRef: React.RefObject; /** Props to spread on the container element. */ containerProps: OverlayContainerProps; /** * Pre-rendered scrim element — place inside the container. * Only available when `content` is provided in options. */ element: ReactElement | null; /** * Render function for the overlay scrim. Use when you want to * control where/when the scrim mounts, or pass dynamic content. * Same pattern as useTooltip's renderTooltip. */ renderOverlay: (children: ReactNode) => ReactElement; } // ============================================================================= // Hook // ============================================================================= /** * Hook for overlay behavior on an existing container. * Returns containerRef, containerProps, and scrim element/render function. * * @example * ``` * const overlay = useOverlay({ * showOn: 'hover', * content: