// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file useLightbox.tsx * @input Uses React, Lightbox * @output Exports useLightbox hook * @position Utility hook; wraps Lightbox with trigger props + state management * * Eliminates boilerplate for lightbox usage. Instead of managing isOpen state * and index separately, call open() and the hook handles the rest. * * @example * ``` * const lightbox = useLightbox({ media: photos }); * lightbox.open(2)} /> * {lightbox.element} * ``` */ import { useState, useCallback, useMemo, type ReactNode } from 'react'; import { Lightbox, type LightboxProps, type LightboxMedia } from "./Lightbox.js"; type LightboxOptions = Omit; export interface UseLightboxOptions extends LightboxOptions { /** Media to display in the lightbox. */ media: LightboxMedia | LightboxMedia[]; } export interface UseLightboxReturn { /** Open the lightbox, optionally at a specific gallery index. */ open: (index?: number) => void; /** Close the lightbox. */ close: () => void; /** Whether the lightbox is currently open. */ isOpen: boolean; /** Current gallery index. */ index: number; /** Render this in your JSX tree. */ element: ReactNode; /** Props to spread on a trigger element for accessibility. */ triggerProps: { role: 'button'; tabIndex: 0; 'aria-haspopup': 'dialog'; onClick: () => void; onKeyDown: (e: React.KeyboardEvent) => void; }; /** Returns trigger props that open at a specific gallery index. */ getTriggerProps: (index: number) => { role: 'button'; tabIndex: 0; 'aria-haspopup': 'dialog'; onClick: () => void; onKeyDown: (e: React.KeyboardEvent) => void; }; } /** * Hook for lightbox with trigger props and state management. * * @example * ``` * const lightbox = useLightbox({ media: photos }); * {photos.map((photo, i) => ( * {photo.alt} * ))} * {lightbox.element} * ``` */ export function useLightbox(options: UseLightboxOptions): UseLightboxReturn { const { media, ...lightboxProps } = options; const [isOpen, setIsOpen] = useState(false); const [index, setIndex] = useState(0); const open = useCallback((i: number = 0) => { setIndex(i); setIsOpen(true); }, []); const close = useCallback(() => { setIsOpen(false); }, []); const triggerProps = useMemo(() => ({ role: 'button' as const, tabIndex: 0 as const, 'aria-haspopup': 'dialog' as const, onClick: () => open(), onKeyDown: (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } } }), [open]); const getTriggerProps = useCallback((i: number) => ({ role: 'button' as const, tabIndex: 0 as const, 'aria-haspopup': 'dialog' as const, onClick: () => open(i), onKeyDown: (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(i); } } }), [open]); const element = useMemo(() => { if (!nextOpen) { setIsOpen(false); } }} media={media} index={index} onIndexChange={setIndex} {...lightboxProps} />, [isOpen, media, index, lightboxProps]); return { open, close, isOpen, index, element, triggerProps, getTriggerProps }; }