返回 DeepSeek-Reasonix
RiskConfirmation.tsx
根目录 / desktop / frontend / src / components / RiskConfirmation.tsx
1 import { useEffect, useId, useLayoutEffect, useRef } from "react";
2 import { CircleAlert, X } from "lucide-react";
3 import { createPortal } from "react-dom";
4
5 export type RiskConfirmationProps = {
6 open: boolean;
7 title: string;
8 description: string;
9 acknowledgeLabel: string;
10 cancelLabel: string;
11 closeLabel: string;
12 confirmLabel: string;
13 acknowledged: boolean;
14 disabled?: boolean;
15 onAcknowledgedChange: (acknowledged: boolean) => void;
16 onCancel: () => void;
17 onConfirm: () => void;
18 };
19
20 /** Explicit acknowledgement gate for sensitive choices such as Full access. */
21 export function RiskConfirmation({
22 open,
23 title,
24 description,
25 acknowledgeLabel,
26 cancelLabel,
27 closeLabel,
28 confirmLabel,
29 acknowledged,
30 disabled = false,
31 onAcknowledgedChange,
32 onCancel,
33 onConfirm,
34 }: RiskConfirmationProps) {
35 const titleId = useId();
36 const descriptionId = useId();
37 const checkboxRef = useRef<HTMLInputElement>(null);
38 const cancelRef = useRef<HTMLButtonElement>(null);
39 const confirmRef = useRef<HTMLButtonElement>(null);
40 const closeRef = useRef<HTMLButtonElement>(null);
41 const restoreFocusRef = useRef<HTMLElement | null>(null);
42
43 useLayoutEffect(() => {
44 if (!open) return;
45 const activeElement = document.activeElement;
46 restoreFocusRef.current = activeElement && "focus" in activeElement ? activeElement as HTMLElement : null;
47 checkboxRef.current?.focus();
48 return () => {
49 if (restoreFocusRef.current?.isConnected) restoreFocusRef.current.focus();
50 restoreFocusRef.current = null;
51 };
52 }, [open]);
53
54 useEffect(() => {
55 if (!open) return;
56 const onKeyDown = (event: KeyboardEvent) => {
57 if (event.key === "Escape") {
58 event.preventDefault();
59 event.stopPropagation();
60 onCancel();
61 return;
62 }
63 if (event.key !== "Tab") return;
64 const focusable = [closeRef.current, checkboxRef.current, cancelRef.current, confirmRef.current]
65 .filter((node): node is HTMLButtonElement | HTMLInputElement => node !== null && !node.disabled);
66 if (focusable.length === 0) return;
67 const first = focusable[0];
68 const last = focusable[focusable.length - 1];
69 if (!first || !last) return;
70 if (event.shiftKey && document.activeElement === first) {
71 event.preventDefault();
72 last.focus();
73 } else if (!event.shiftKey && document.activeElement === last) {
74 event.preventDefault();
75 first.focus();
76 }
77 };
78 document.addEventListener("keydown", onKeyDown, { capture: true });
79 return () => document.removeEventListener("keydown", onKeyDown, { capture: true });
80 }, [onCancel, open]);
81
82 if (!open) return null;
83
84 return createPortal(
85 <div className="risk-confirmation" data-app-overlay="" role="presentation">
86 <div className="risk-confirmation__mask" aria-hidden="true" onMouseDown={onCancel} />
87 <section
88 className="risk-confirmation__dialog"
89 role="dialog"
90 aria-modal="true"
91 aria-labelledby={titleId}
92 aria-describedby={descriptionId}
93 >
94 <div className="risk-confirmation__content">
95 <header className="risk-confirmation__header">
96 <h2 id={titleId}>{title}</h2>
97 <button ref={closeRef} type="button" className="risk-confirmation__close" aria-label={closeLabel} disabled={disabled} onClick={onCancel}>
98 <X size={18} strokeWidth={1.8} aria-hidden="true" />
99 </button>
100 </header>
101 <div className="risk-confirmation__body">
102 <div className="risk-confirmation__warning">
103 <CircleAlert size={18} strokeWidth={2} aria-hidden="true" />
104 <p id={descriptionId}>{description}</p>
105 </div>
106 <label className="risk-confirmation__acknowledgement">
107 <input
108 ref={checkboxRef}
109 type="checkbox"
110 checked={acknowledged}
111 disabled={disabled}
112 onChange={(event) => onAcknowledgedChange(event.currentTarget.checked)}
113 />
114 <span>{acknowledgeLabel}</span>
115 </label>
116 </div>
117 </div>
118 <footer className="risk-confirmation__footer">
119 <button ref={cancelRef} type="button" className="btn risk-confirmation__cancel" disabled={disabled} onClick={onCancel}>{cancelLabel}</button>
120 <button
121 ref={confirmRef}
122 type="button"
123 className="btn btn--primary risk-confirmation__confirm"
124 disabled={disabled || !acknowledged}
125 onClick={onConfirm}
126 >
127 {confirmLabel}
128 </button>
129 </footer>
130 </section>
131 </div>,
132 document.body,
133 );
134 }
135
135 lines Plain Text