بنقرة واحدة
frontend-hotkeys
Use when creating or modifying keyboard shortcuts/hotkeys in frontend code
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Use when creating or modifying keyboard shortcuts/hotkeys in frontend code
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Use when deploying Cloudflare Workers, managing R2 storage, or working with Cloudflare infrastructure
Use when working with ANTD components, theme tokens, icons, forms, or feedback components (message/notification/modal)
Use when adding, referencing, or serving static assets (images, fonts, videos, 3D models) through the R2 CDN pipeline with type-safe imports
Use when writing or reviewing JavaScript/TypeScript code for style patterns like concise arrows, inline handlers, expression formatting, or when tempted to use eslint-disable
Use when working with environment variables in frontend code
Use when creating or using TanStack Query mutations for data modifications
| name | frontend-hotkeys |
| description | Use when creating or modifying keyboard shortcuts/hotkeys in frontend code |
Use react-hotkeys-hook for all keyboard shortcuts.
Naming convention: See frontend-naming-conventions for the useHotkeys_[ComponentName] pattern.
pnpm add react-hotkeys-hook
import { useHotkeys } from "react-hotkeys-hook";
export const useHotkeys_PageScene = () => {
const pPageScene = useProvider_Page_Scene();
// Simple hotkey (blocked in inputs by default)
useHotkeys(
"space",
() => {
pPageScene.setState((prev) => ({
timeline_isPlaying: !prev.timeline_isPlaying,
}));
},
{ preventDefault: true }
);
// Escape - ALWAYS use enableOnFormTags: true
useHotkeys(
"escape",
() => {
pPageScene.setState({ nodeSelected: null });
},
{ enableOnFormTags: true }
);
// Conditional hotkey (check inside callback)
useHotkeys(
"f",
() => {
if (pPageScene.state.nodeSelected?.category !== "shot") return;
pPageScene.togglePreviewFullscreenRef.current?.();
},
{ preventDefault: true }
);
};
| Option | Default | Description |
|---|---|---|
preventDefault | false | Prevent browser default (use for Space, F, etc.) |
enableOnFormTags | false | Allow in input/textarea/select |
enableOnContentEditable | false | Allow in contentEditable elements |
enabled | true | Conditionally enable/disable the hotkey |
keyup | false | Trigger on keyup instead of keydown |
keydown | true | Trigger on keydown |
Hotkeys are blocked in input/textarea/select by default. This is the desired behavior.
// This will NOT fire when user is typing in an input
useHotkeys("space", () => togglePlayback());
Escape should always work, even in inputs. Always add enableOnFormTags: true:
// ✅ Escape works everywhere
useHotkeys("escape", () => cancelAction(), { enableOnFormTags: true });
For keys with browser defaults (Space scrolls, F can trigger form), use preventDefault:
useHotkeys('space', () => { ... }, { preventDefault: true });
useHotkeys('f', () => { ... }, { preventDefault: true });
RULE: If you read external values, you MUST use useHotkeysWithDeps to declare deps.
import { useHotkeysWithDeps } from "@/hooks/hotkeys";
// ✅ Reading external values - MUST use useHotkeysWithDeps
useHotkeysWithDeps(
() => {
if (nodeSelected?.category === "shot") {
deleteKeyframes(nodeSelected.selectedKeyframeIds);
}
},
[nodeSelected, deleteKeyframes], // ESLint enforces this
"delete"
);
RULE: If you have NO external dependencies, use native useHotkeys.
import { useHotkeys } from "react-hotkeys-hook";
// ✅ No external deps - use native useHotkeys with functional update
useHotkeys(
"space",
() => {
pPageScene.setState((prev) => ({
timeline_isPlaying: !prev.timeline_isPlaying,
}));
},
{ preventDefault: true }
);
Signature comparison:
// Native (no deps):
useHotkeys(keys, callback, options);
// With deps (ESLint enforced):
useHotkeysWithDeps(callback, deps, keys, options);
| Scenario | Hook to Use |
|---|---|
| Reading external state/values | useHotkeysWithDeps (MUST declare deps) |
Functional update only (prev => ...) | useHotkeys (no deps needed) |
| Calling functions with external IDs | useHotkeysWithDeps (MUST declare deps) |
Two patterns for conditional hotkeys:
// Pattern A: Check inside callback (simpler)
useHotkeys("f", () => {
if (nodeSelected?.category !== "shot") return;
toggleFullscreen();
});
// Pattern B: Use enabled option (prevents callback entirely)
useHotkeys(
"f",
() => toggleFullscreen(),
{
enabled: nodeSelected?.category === "shot",
},
[nodeSelected]
);
Use Pattern A for simple checks. Use Pattern B when the check itself is expensive.
Colocate hotkey hooks with their component:
Page_Scene/
├── Page_Scene.tsx # calls useHotkeys_PageScene()
├── useHotkeys_PageScene.ts # Space, Escape, Delete, F
└── PageScene_Timeline/
├── PageScene_Timeline.tsx # calls useHotkeys_PageScene_Timeline()
└── useHotkeys_PageScene_Timeline.ts # Delete for keyframes
// ❌ Wrong - manual listener (old pattern)
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.code === 'Space') { ... }
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, []);
// ✅ Correct - use library
useHotkeys('space', () => { ... }, { preventDefault: true });
// ❌ Wrong - forgetting enableOnFormTags for Escape
useHotkeys("escape", () => cancel());
// ✅ Correct - Escape works in inputs
useHotkeys("escape", () => cancel(), { enableOnFormTags: true });
// ❌ Wrong - reading external value without declaring deps
const [count, setCount] = useState(0);
useHotkeys("space", () => console.log(count)); // count is stale!
// ✅ Correct - reading external value, MUST use useHotkeysWithDeps
useHotkeysWithDeps(() => console.log(count), [count], "space");
// ✅ Correct - no external deps, use native useHotkeys
useHotkeys("space", () => setCount((prev) => prev + 1));
Do NOT use react-hotkeys-hook for:
useFrame)These need manual keydown/keyup listeners to track held keys:
// This is CORRECT for continuous input (FlyControls, etc.)
const keys = useRef({ w: false, a: false, s: false, d: false });
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const key = e.key.toLowerCase();
if (key in keys.current) keys.current[key] = true;
};
const handleKeyUp = (e: KeyboardEvent) => {
const key = e.key.toLowerCase();
if (key in keys.current) keys.current[key] = false;
};
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("keyup", handleKeyUp);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("keyup", handleKeyUp);
};
}, []);
useFrame(() => {
if (keys.current.w) camera.position.add(forward);
// ...
});
// Modifier + key
useHotkeys("ctrl+s", () => save(), { preventDefault: true });
useHotkeys("cmd+s", () => save(), { preventDefault: true }); // macOS
// Multiple keys (comma-separated)
useHotkeys("ctrl+s, cmd+s", () => save(), { preventDefault: true });
// Array syntax
useHotkeys(["ctrl+z", "cmd+z"], () => undo());
Not used initially. Add if needed for complex modal/state management:
import { HotkeysProvider, useHotkeysContext } from 'react-hotkeys-hook';
// Wrap app
<HotkeysProvider initiallyActiveScopes={['global']}>
<App />
</HotkeysProvider>
// Define scoped hotkey
useHotkeys('space', () => play(), { scopes: 'timeline' });
// Enable/disable scopes
const { enableScope, disableScope } = useHotkeysContext();
disableScope('timeline'); // Space no longer triggers