fix(ui): only close a modal when the backdrop click starts on the backdrop

Pressing the mouse inside a dialog (e.g. to select text in an input), dragging
out, and releasing on the backdrop wrongly dismissed it: the click event fires
on the common ancestor of mousedown+mouseup (the backdrop), so the outside-click
handler ran. Track whether the press started on the backdrop and only close when
it both starts and ends there. Fixes all shared-Modal dialogs at once (downloads
editor/share, confirm, Plex collections + add-to-playlist, …).
This commit is contained in:
npeter83 2026-07-07 22:28:33 +02:00
parent 33fad8911b
commit 374ad4ddc4

View file

@ -23,6 +23,11 @@ export default function Modal({
// Browser/mouse Back closes the topmost modal instead of navigating away. // Browser/mouse Back closes the topmost modal instead of navigating away.
useBackToClose(onClose); useBackToClose(onClose);
// Backdrop-click close must only fire when the press STARTED on the backdrop too. Otherwise a
// drag that begins inside the dialog (e.g. selecting text in an input) and is released out on
// the backdrop wrongly dismisses — the click event bubbles to the common ancestor (the backdrop).
const pressedOnBackdrop = useRef(false);
// Keep a stable handler across renders so the stack id is assigned once per mount. // Keep a stable handler across renders so the stack id is assigned once per mount.
const onCloseRef = useRef(onClose); const onCloseRef = useRef(onClose);
onCloseRef.current = onClose; onCloseRef.current = onClose;
@ -48,13 +53,17 @@ export default function Modal({
return createPortal( return createPortal(
<div <div
className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/70 backdrop-blur-sm" className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/70 backdrop-blur-sm"
onClick={onClose} onMouseDown={(e) => {
pressedOnBackdrop.current = e.target === e.currentTarget;
}}
onClick={(e) => {
if (pressedOnBackdrop.current && e.target === e.currentTarget) onClose();
}}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
> >
<div <div
className={`glass relative w-full ${maxWidth} max-h-full overflow-y-auto rounded-2xl shadow-2xl`} className={`glass relative w-full ${maxWidth} max-h-full overflow-y-auto rounded-2xl shadow-2xl`}
onClick={(e) => e.stopPropagation()}
> >
<div className="flex items-start justify-between gap-3 p-5 pb-3"> <div className="flex items-start justify-between gap-3 p-5 pb-3">
<h2 className="text-lg font-semibold leading-snug">{title}</h2> <h2 className="text-lg font-semibold leading-snug">{title}</h2>