Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | 22x 168x 168x 168x 168x 168x 113x 35x 23x 6x 6x 113x 113x 168x 73x 73x 23x 17x 17x | // SPDX-License-Identifier: GPL-3.0-only
// Copyright (C) 2026 Max Boksem. See NOTICE for additional terms under GPLv3 section 7.
import { useEffect, useRef, type ReactNode } from "react";
import { isTopFocusTrap, useFocusTrap } from "@/hooks/useFocusTrap";
import { useLatestRef } from "@/hooks/useLatestRef";
import { cn } from "@/lib/utils";
type DialogName = { labelledBy: string; label?: never } | { label: string; labelledBy?: never };
type DialogProps = DialogName & {
onClose: () => void;
/** Added to the backdrop, a fixed overlay over the whole window by default. */
overlayClassName?: string;
/** The dialog box itself. */
className?: string;
children: ReactNode;
};
/**
* A modal over a dimmed backdrop. A click on the backdrop or Escape closes it,
* and focus moves into it and stays there until it closes. With one dialog
* open over another, Escape closes only the top one.
*/
export const Dialog = ({
onClose,
overlayClassName,
className,
children,
labelledBy,
label,
}: DialogProps) => {
const overlayRef = useRef<HTMLDivElement>(null);
const dialogRef = useRef<HTMLDivElement>(null);
useFocusTrap(dialogRef);
const onCloseRef = useLatestRef(onClose);
// Escape pressed while focus is outside the dialog, such as on the page
// after the focused control went away. It runs before the page's own
// handlers so the screen behind never sees the key. Keys from inside the
// dialog are left to the handler on the backdrop, so a control inside can
// use Escape first.
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key !== "Escape" || !isTopFocusTrap(dialogRef.current)) return;
if (e.target instanceof Node && overlayRef.current?.contains(e.target)) return;
e.stopPropagation();
onCloseRef.current();
};
document.addEventListener("keydown", handleKey, true);
return () => document.removeEventListener("keydown", handleKey, true);
}, [onCloseRef]);
return (
<div
ref={overlayRef}
className={cn(
"fixed inset-0 z-50 flex items-center justify-center bg-black/60",
overlayClassName
)}
onClick={(e) => {
// A dialog opened from a clickable row, like a channel card, would
// otherwise pass its clicks on to that row.
e.stopPropagation();
if (e.target === e.currentTarget) onClose();
}}
onKeyDown={(e) => {
if (e.key !== "Escape") return;
// The screen behind may have its own Escape shortcut, like leaving full screen.
e.stopPropagation();
// A control inside, such as a field, can keep Escape for itself.
if (!e.defaultPrevented) onClose();
}}
>
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby={labelledBy}
aria-label={label}
tabIndex={-1}
className={cn("focus:outline-hidden", className)}
>
{children}
</div>
</div>
);
};
|