diff --git a/src/shared/ui/ImageLightbox/ui/ImageLightbox.test.tsx b/src/shared/ui/ImageLightbox/ui/ImageLightbox.test.tsx index 07e1537..3610199 100644 --- a/src/shared/ui/ImageLightbox/ui/ImageLightbox.test.tsx +++ b/src/shared/ui/ImageLightbox/ui/ImageLightbox.test.tsx @@ -41,21 +41,22 @@ describe('ImageLightbox', () => { it('clicking the close button closes the dialog', () => { render(); - fireEvent.click(screen.getByRole('button', { name: /close/i })); + fireEvent.click(screen.getByRole('button', { name: 'My Project' })); // open first + fireEvent.click(screen.getByRole('button', { name: /close/i, hidden: true })); expect(HTMLDialogElement.prototype.close).toHaveBeenCalledTimes(1); }); it('clicking the backdrop (dialog element itself) closes the dialog', () => { render(); - const dialog = document.querySelector('dialog')!; + const dialog = document.querySelector('dialog') as HTMLDialogElement; fireEvent.click(dialog, { target: dialog }); expect(HTMLDialogElement.prototype.close).toHaveBeenCalledTimes(1); }); it('clicking inside the dialog (not backdrop) does not close', () => { render(); - const dialog = document.querySelector('dialog')!; - const inner = dialog.querySelector('div')!; + const dialog = document.querySelector('dialog') as HTMLDialogElement; + const inner = dialog.querySelector('div') as HTMLDivElement; fireEvent.click(inner); expect(HTMLDialogElement.prototype.close).not.toHaveBeenCalled(); }); diff --git a/src/shared/ui/ImageLightbox/ui/ImageLightbox.tsx b/src/shared/ui/ImageLightbox/ui/ImageLightbox.tsx index 1789e74..ba4bff9 100644 --- a/src/shared/ui/ImageLightbox/ui/ImageLightbox.tsx +++ b/src/shared/ui/ImageLightbox/ui/ImageLightbox.tsx @@ -1,5 +1,9 @@ 'use client'; +import Image from 'next/image'; +import { useRef } from 'react'; +import { cn } from '$shared/lib'; + type Props = { /** * Image source URL @@ -18,6 +22,65 @@ type Props = { /** * Clickable image thumbnail that opens a fullscreen brutalist dialog on click. */ -export function ImageLightbox(_props: Props) { - return null; +export function ImageLightbox({ src, alt, className }: Props) { + const dialogRef = useRef(null); + + function open() { + dialogRef.current?.showModal(); + } + + function close() { + dialogRef.current?.close(); + } + + function handleBackdropClick(e: React.MouseEvent) { + if (e.target === e.currentTarget) { + close(); + } + } + + /** + * Keyboard equivalent of backdrop click — closes the dialog when the user + * activates the backdrop area (dialog element itself) via Enter or Space. + * ESC is handled natively by showModal(); this covers explicit backdrop activation. + */ + function handleBackdropKeyUp(e: React.KeyboardEvent) { + if (e.target === e.currentTarget && (e.key === 'Enter' || e.key === ' ')) { + close(); + } + } + + return ( + <> + + + +
+ {/* aria-hidden: the dialog element itself carries the accessible label */} + {alt} +
+ +
+ + ); }