| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364 |
- import { useEffect, useRef } from 'react'
- import { createPortal } from 'react-dom'
- import { IconCloseOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
- import css from './ImageLightbox.module.css'
- /** Lightbox strings the owner resolves from its own locale namespace. */
- export interface ImageLightboxLabels {
- /** Accessible name of the preview dialog. */
- dialog: string
- /** Accessible label of the close control. */
- close: string
- }
- /**
- * Document-level original-image preview opened by clicking a thumbnail.
- * Closes on Escape, backdrop press, or the close control, and restores focus
- * to the opener on unmount. Rendered through a body portal: an opener inside
- * a transformed or filtered ancestor would otherwise trap the fixed backdrop
- * in that ancestor's box instead of covering the viewport.
- *
- * @param props.src - the original image URL.
- * @param props.alt - the image's alt text.
- * @param props.labels - dialog and close-control strings.
- * @param props.onClose - dismiss callback owned by the opener.
- * @returns the modal preview dialog.
- */
- export function ImageLightbox({ src, alt, labels, onClose }: {
- src: string
- alt: string
- labels: ImageLightboxLabels
- onClose: () => void
- }) {
- const closeRef = useRef<HTMLButtonElement | null>(null)
- const restoreRef = useRef<HTMLElement | null>(null)
- useEffect(() => {
- restoreRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null
- closeRef.current?.focus()
- const onKeyDown = (event: globalThis.KeyboardEvent): void => {
- if (event.key === 'Escape') onClose()
- }
- window.addEventListener('keydown', onKeyDown)
- return () => {
- window.removeEventListener('keydown', onKeyDown)
- restoreRef.current?.focus()
- }
- }, [onClose])
- return createPortal(
- <div
- className={css.backdrop}
- role="dialog"
- aria-modal="true"
- aria-label={labels.dialog}
- >
- <div className={css.mask} aria-hidden="true" onMouseDown={onClose} />
- <img className={css.image} src={src} alt={alt} />
- <button ref={closeRef} type="button" className={css.close} aria-label={labels.close} onClick={onClose}>
- <IconCloseOutline16 size={16} />
- </button>
- </div>,
- document.body,
- )
- }
|