返回 DeepSeek-Reasonix
ShortcutsCheatsheet.tsx
根目录 / desktop / frontend / src / components / ShortcutsCheatsheet.tsx
1 import { useEffect, useMemo, useRef } from "react";
2 import {
3 resolvedShortcutCombo,
4 shortcutDefinitions,
5 type ShortcutPlatform,
6 type ShortcutSection,
7 } from "../lib/keyboardShortcuts";
8 import type { DictKey, Translator } from "../lib/i18n";
9 import { ModalCloseButton } from "./ModalCloseButton";
10 import { ShortcutComboDisplay } from "./ShortcutComboDisplay";
11
12 const SECTION_ORDER: ShortcutSection[] = ["global", "session", "view", "tools", "help"];
13 const SECTION_LABEL_KEYS: Record<ShortcutSection, DictKey> = {
14 global: "shortcuts.section.global",
15 session: "shortcuts.section.session",
16 view: "shortcuts.section.view",
17 tools: "shortcuts.section.tools",
18 help: "shortcuts.section.help",
19 };
20
21 export function ShortcutsCheatsheet({
22 open,
23 platform,
24 onClose,
25 t,
26 }: {
27 open: boolean;
28 platform: ShortcutPlatform;
29 onClose: () => void;
30 t: Translator;
31 }) {
32 const closeRef = useRef<HTMLButtonElement>(null);
33 const restoreFocusRef = useRef<HTMLElement | null>(null);
34 const groups = useMemo(() => {
35 return SECTION_ORDER.map((section) => ({
36 section,
37 items: shortcutDefinitions().filter((definition) => definition.section === section),
38 })).filter((group) => group.items.length > 0);
39 }, []);
40
41 useEffect(() => {
42 if (open) {
43 restoreFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
44 requestAnimationFrame(() => closeRef.current?.focus());
45 return;
46 }
47 if (restoreFocusRef.current?.isConnected) restoreFocusRef.current.focus();
48 restoreFocusRef.current = null;
49 }, [open]);
50
51 useEffect(() => {
52 if (!open) return;
53 const onKey = (event: KeyboardEvent) => {
54 if (event.key !== "Escape") return;
55 event.preventDefault();
56 onClose();
57 };
58 document.addEventListener("keydown", onKey, { capture: true });
59 return () => document.removeEventListener("keydown", onKey, { capture: true });
60 }, [open, onClose]);
61
62 if (!open) return null;
63
64 return (
65 <div className="drawer-backdrop shortcuts-cheatsheet-backdrop" onClick={onClose} role="presentation">
66 <aside
67 className="drawer drawer--wide shortcuts-cheatsheet"
68 role="dialog"
69 aria-modal="true"
70 aria-labelledby="shortcuts-cheatsheet-title"
71 onClick={(event) => event.stopPropagation()}
72 >
73 <header className="drawer__head">
74 <div>
75 <div id="shortcuts-cheatsheet-title" className="drawer__title">
76 {t("shortcuts.cheatsheetTitle")}
77 </div>
78 <div className="drawer__summary">{t("shortcuts.cheatsheetSummary")}</div>
79 </div>
80 <ModalCloseButton ref={closeRef} label={t("common.close")} onClick={onClose} />
81 </header>
82 <div className="drawer__body shortcuts-cheatsheet__body">
83 {groups.map((group) => (
84 <section className="shortcuts-cheatsheet__section" key={group.section}>
85 <h3>{t(SECTION_LABEL_KEYS[group.section])}</h3>
86 <div className="shortcuts-cheatsheet__list">
87 {group.items.map((definition) => (
88 <div className="shortcuts-cheatsheet__row" key={definition.action}>
89 <ShortcutComboDisplay
90 as="kbd"
91 combo={resolvedShortcutCombo(definition.action, platform)}
92 platform={platform}
93 />
94 <div>
95 <strong>{t(definition.labelKey)}</strong>
96 <span className="shortcuts-cheatsheet__desc">{t(definition.descriptionKey)}</span>
97 </div>
98 </div>
99 ))}
100 </div>
101 </section>
102 ))}
103 </div>
104 </aside>
105 </div>
106 );
107 }
108
108 lines Plain Text