diff --git a/src/components/Lightbox/Lightbox.test.tsx b/src/components/Lightbox/Lightbox.test.tsx index 1e4ba776..50a08c1f 100644 --- a/src/components/Lightbox/Lightbox.test.tsx +++ b/src/components/Lightbox/Lightbox.test.tsx @@ -40,6 +40,15 @@ function getMediaViewport(element: HTMLElement): HTMLElement { return parent; } +function getBackdropContainer(): HTMLElement { + // eslint-disable-next-line testing-library/no-node-access -- the layout container is the lightbox's backdrop hit target + const container = screen.getByRole('dialog').firstElementChild; + if (!(container instanceof HTMLElement)) { + throw new Error('Expected the lightbox dialog to have a layout container.'); + } + return container; +} + beforeAll(() => { Object.defineProperty(HTMLDialogElement.prototype, 'showModal', { configurable: true, @@ -124,7 +133,9 @@ describe('Lightbox', () => { const onOpenChange = vi.fn(); render(); - fireEvent.click(screen.getByRole('dialog', {name: 'Media lightbox'})); + const backdropContainer = getBackdropContainer(); + fireEvent.pointerDown(backdropContainer); + fireEvent.click(backdropContainer, {detail: 1}); expect(onOpenChange).toHaveBeenCalledWith(false); const cancelEvent = new Event('cancel', {cancelable: true}); @@ -136,6 +147,25 @@ describe('Lightbox', () => { expect(onOpenChange).toHaveBeenCalledWith(false); }); + it('does not close when a pan drag ends on the backdrop', () => { + const onOpenChange = vi.fn(); + render( + , + ); + + const viewport = getMediaViewport( + screen.getByRole('img', {name: 'First image'}), + ); + const backdropContainer = getBackdropContainer(); + fireEvent.doubleClick(viewport); + fireEvent.pointerDown(viewport, {clientX: 100, clientY: 100}); + fireEvent.pointerMove(window, {clientX: 125, clientY: 125}); + fireEvent.pointerUp(backdropContainer, {clientX: 150, clientY: 150}); + fireEvent.click(backdropContainer, {clientX: 150, clientY: 150, detail: 1}); + + expect(onOpenChange).not.toHaveBeenCalled(); + }); + it('navigates galleries with arrow keys and buttons while respecting bounds', async () => { const user = userEvent.setup(); const onIndexChange = vi.fn(); diff --git a/src/components/Lightbox/Lightbox.tsx b/src/components/Lightbox/Lightbox.tsx index 9e94f2c8..9288a20f 100644 --- a/src/components/Lightbox/Lightbox.tsx +++ b/src/components/Lightbox/Lightbox.tsx @@ -9,7 +9,6 @@ import { useRef, useState, type CSSProperties, - type MouseEvent as ReactMouseEvent, type ReactNode, type Ref, } from 'react'; @@ -19,6 +18,7 @@ import {LayerContext} from 'internal/LayerContext'; import {LogicalChevronEnd, LogicalChevronStart} from 'internal/LogicalChevron'; import isNonEmptyReactNode from 'internal/isNonEmptyReactNode'; import {mergeRefs} from 'internal/mergeRefs'; +import {useBackdropDismiss} from 'internal/useBackdropDismiss'; import {useEscapeDismiss} from 'internal/useEscapeDismiss'; import {useIsomorphicLayoutEffect} from 'internal/useIsomorphicLayoutEffect'; import {useScrollLock} from 'internal/useScrollLock'; @@ -128,6 +128,7 @@ export function Lightbox({ style, }: LightboxProps): React.JSX.Element { const dialogRef = useRef(null); + const containerRef = useRef(null); const triggerRef = useRef(null); const [uncontrolledIndex, setUncontrolledIndex] = useState(defaultIndex); const [zoom, setZoom] = useState(1); @@ -240,6 +241,11 @@ export function Lightbox({ const close = useCallback(() => { onOpenChange(false); }, [onOpenChange]); + const backdropDismiss = useBackdropDismiss({ + isBackdropEvent: event => event.target === containerRef.current, + isEnabled: isOpen, + onDismiss: close, + }); const escapeDismiss = useEscapeDismiss({ getElement: () => dialogRef.current, isEnabled: isOpen, @@ -266,11 +272,7 @@ export function Lightbox({ event.preventDefault(); close(); }} - onClick={(event: ReactMouseEvent) => { - if (event.target === event.currentTarget) { - close(); - } - }} + onClick={backdropDismiss.onClick} onKeyDown={event => { if (event.key === 'ArrowLeft') { event.preventDefault(); @@ -280,10 +282,11 @@ export function Lightbox({ goNext(); } }} + onPointerDown={backdropDismiss.onPointerDown} ref={mergeRefs(ref, dialogRef)} style={style}> -
+