Something went wrong. Try again.
[mirror] A local-first desktop markdown writer with grammar checking, at proto integration, and more github.com/stormlightlabs/writer/releases
Something went wrong. Try again.
4.8 kB · 159 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160import { DIALOG, DIALOG_MOTION_PRESETS, NO_MOTION_TRANSITION } from "$constants";import { useSkipAnimation } from "$hooks/useMotion";import { DialogMotionPreset } from "$types";import { cn } from "$utils/tw";import { AnimatePresence, motion } from "motion/react";import { useEffect, useMemo, useRef } from "react";import type { CSSProperties, ReactNode } from "react";
type DialogProps = { isOpen: boolean; children: ReactNode; onClose?: () => void; ariaLabel?: string; showBackdrop?: boolean; closeOnBackdrop?: boolean; backdropClassName?: string; containerClassName?: string; containerStyle?: CSSProperties; panelClassName?: string; panelStyle?: CSSProperties; motionPreset?: DialogMotionPreset;};
const FOCUSABLE_SELECTOR = "button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])";
function getFocusableElements(root: HTMLElement): HTMLElement[] { return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter((element) => !element.hasAttribute("disabled") && element.getAttribute("aria-hidden") !== "true" );}
export function Dialog( { isOpen, children, onClose, ariaLabel, showBackdrop = true, closeOnBackdrop = true, backdropClassName, containerClassName, containerStyle, panelClassName, panelStyle, motionPreset = "scale", }: DialogProps,) { const skipAnimation = useSkipAnimation(); const motionConfig = DIALOG_MOTION_PRESETS[motionPreset]; const panelRef = useRef<HTMLDivElement | null>(null); const backdropTransition = useMemo(() => skipAnimation ? NO_MOTION_TRANSITION : DIALOG.BACKDROP.FADE_TRANSITION, [ skipAnimation, ]); const surfaceTransition = useMemo(() => skipAnimation ? NO_MOTION_TRANSITION : DIALOG.SURFACE.TRANSITION, [ skipAnimation, ]);
const handleBackdropClick = closeOnBackdrop ? onClose : undefined;
useEffect(() => { if (!isOpen || typeof globalThis.addEventListener !== "function") { return; }
const panel = panelRef.current; if (!panel) { return; }
const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null; const focusable = getFocusableElements(panel); const firstFocusable = focusable[0]; const fallbackFocusable = panel; (firstFocusable ?? fallbackFocusable).focus();
const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape" && onClose) { onClose(); return; }
if (event.key !== "Tab") { return; }
const activePanel = panelRef.current; if (!activePanel) { return; }
const cycleTargets = getFocusableElements(activePanel); if (cycleTargets.length === 0) { event.preventDefault(); activePanel.focus(); return; }
const first = cycleTargets[0]; const last = cycleTargets.at(-1); const activeElement = document.activeElement;
if (event.shiftKey && activeElement === first) { event.preventDefault(); last?.focus(); } else if (!event.shiftKey && activeElement === last) { event.preventDefault(); first.focus(); } };
globalThis.addEventListener("keydown", handleKeyDown); return () => { globalThis.removeEventListener("keydown", handleKeyDown); previousFocus?.focus(); }; }, [isOpen, onClose]);
return ( <AnimatePresence> {isOpen && ( <div className={cn("fixed inset-0 z-50 pointer-events-none", containerClassName)} style={containerStyle} role="presentation"> {showBackdrop && ( <motion.button type="button" aria-label="Close dialog" className={cn( "absolute inset-0 z-0 border-none bg-black/45 p-0 m-0 pointer-events-auto", backdropClassName, )} onClick={handleBackdropClick} initial={DIALOG.BACKDROP.FADE_MOTION.initial} animate={DIALOG.BACKDROP.FADE_MOTION.animate} exit={DIALOG.BACKDROP.FADE_MOTION.exit} transition={backdropTransition} /> )}
<motion.div role="dialog" aria-label={ariaLabel} aria-modal={showBackdrop} tabIndex={-1} ref={panelRef} className={cn("relative z-10 pointer-events-auto", panelClassName)} style={panelStyle} initial={motionConfig.initial} animate={motionConfig.animate} exit={motionConfig.exit} transition={surfaceTransition}> {children} </motion.div> </div> )} </AnimatePresence> );}