返回 DeepSeek-Reasonix
ProviderDialog.tsx
根目录 / desktop / frontend / src / components / ProviderDialog.tsx
1 import { useEffect, useId, useLayoutEffect, useRef, type ReactNode } from "react";
2 import { createPortal } from "react-dom";
3 import { useT } from "../lib/i18n";
4 import { ModalCloseButton } from "./ModalCloseButton";
5
6 /** A settings child dialog that owns focus and Escape without closing settings. */
7 export function ProviderDialog({ title, children, onClose }: { title: string; children: ReactNode; onClose: () => void }) {
8 const id = useId();
9 const ref = useRef<HTMLDivElement>(null);
10 const closeRef = useRef(onClose);
11 closeRef.current = onClose;
12 const t = useT();
13 useLayoutEffect(() => {
14 const previous = document.activeElement as HTMLElement | null;
15 ref.current?.querySelector<HTMLElement>("input:not(:disabled), select:not(:disabled)")?.focus();
16 return () => { if (previous?.isConnected) previous.focus(); };
17 }, []);
18 useEffect(() => {
19 const keydown = (event: KeyboardEvent) => {
20 if (event.key === "Escape") {
21 event.preventDefault(); event.stopImmediatePropagation(); closeRef.current();
22 }
23 if (event.key === "Tab") {
24 const elements = Array.from(ref.current?.querySelectorAll<HTMLElement>("button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), summary") ?? []);
25 const first = elements[0], last = elements[elements.length - 1];
26 if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
27 else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
28 }
29 };
30 window.addEventListener("keydown", keydown, true);
31 return () => window.removeEventListener("keydown", keydown, true);
32 }, []);
33 return createPortal(<div className="modal-backdrop provider-dialog-backdrop" data-app-overlay="" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
34 <div ref={ref} className="modal provider-dialog" role="dialog" aria-modal="true" aria-labelledby={id}>
35 <header><h3 id={id}>{title}</h3><ModalCloseButton label={t("common.close")} onClick={onClose} /></header>
36 {children}
37 </div>
38 </div>, document.body);
39 }
40
40 lines Plain Text