import { motion, AnimatePresence } from "framer-motion"; import React, { useRef } from "react"; import { TextButton } from "./Buttons/TextButton"; import { useEffect, useState } from "react"; import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"; import { useKeyboardShortcuts } from "@/components/utils/useKeyboardEvents"; import { UAParser } from "ua-parser-js"; const focusedButtonAtom = atom(-1); export const useDisableBodyScroll = (open: boolean) => { useEffect(() => { if (open) { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = "unset"; } }, [open]); }; type OptionalChidrenProps> = T & { children?: React.ReactNode; }; type HeadElementAttr = React.HTMLAttributes; type DivElementAttr = React.HTMLAttributes; type ButtonElementAttr = React.HTMLAttributes; type DialogHeadlineProps = OptionalChidrenProps; type DialogSupportingTextProps = OptionalChidrenProps; type DialogButtonGroupProps = DivElementAttr & { children: React.ReactElement | React.ReactElement[]; close: () => void; }; interface DialogButtonProps extends OptionalChidrenProps { onClick?: React.MouseEventHandler; index?: number; } interface DialogProps extends OptionalChidrenProps { show: boolean; children?: React.ReactNode; } export const DialogHeadline: React.FC = ({ children, className, ...rest }: DialogHeadlineProps) => { return (

{children}

); }; export const DialogSupportingText: React.FC = ({ children, className, ...rest }: DialogHeadlineProps) => { return (
{children}
); }; export const DialogButton: React.FC = ({ children, onClick, index, ...rest }: DialogButtonProps) => { const buttonRef = useRef(null); const focusedButton = useAtomValue(focusedButtonAtom); useEffect(() => { if (!buttonRef.current) return; if (focusedButton === index) buttonRef.current.focus(); }, [focusedButton]); return ( {children} ); }; export const DialogButtonGroup: React.FC = ({ children, close, ...rest }: DialogButtonGroupProps) => { const [focusedButton, setFocusedButton] = useAtom(focusedButtonAtom); const count = React.Children.count(children); useKeyboardShortcuts([ { key: "Tab", callback: () => { setFocusedButton((focusedButton + 1) % count); }, preventDefault: true }, { key: "Escape", callback: close, preventDefault: true } ]); return (
{React.Children.map(children, (child, index) => { if (React.isValidElement(child) && child.type === DialogButton) { return React.cloneElement(child, { index: index }); } return child; })}
); }; const useCompabilityCheck = () => { const [supported, setSupported] = useState(false); useEffect(() => { const parser = new UAParser(navigator.userAgent); const result = parser.getResult(); const { name: browserName, version: browserVersion } = result.browser; let isSupported = false; if (!browserVersion) { return; } const [major] = browserVersion.split(".").map(Number); switch (browserName) { case "Chromium": isSupported = major >= 107; break; case "Firefox": isSupported = major >= 66; break; case "Safari": isSupported = major >= 16; break; default: isSupported = false; break; } setSupported(isSupported); }, []); return supported; }; export const Dialog: React.FC = ({ show, children, className }: DialogProps) => { const dialogRef = useRef(null); const contentRef = useRef(null); const setFocusedButton = useSetAtom(focusedButtonAtom); const isSupported = useCompabilityCheck(); useEffect(() => { if (!contentRef.current || !dialogRef.current) return; const contentHeight = contentRef.current.offsetHeight; const halfSize = (contentHeight + 48) / 2; dialogRef.current.style.top = `calc(50% - ${halfSize}px)`; if (!isSupported) { return; } dialogRef.current.style.transition = "grid-template-rows cubic-bezier(0.05, 0.7, 0.1, 1.0) 0.35s"; if (show) { dialogRef.current.style.gridTemplateRows = "1fr"; } else { dialogRef.current.style.gridTemplateRows = "0.6fr"; } }, [show]); useEffect(() => { setFocusedButton(-1); }, [show]); useDisableBodyScroll(show); return ( {show && (
)}
); };