Skip to content

Commit 5061065

Browse files
authored
[core] Allow disabling the useHotkeys help dialog (#8127)
1 parent 5467683 commit 5061065

3 files changed

Lines changed: 164 additions & 10 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/* !
2+
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
3+
*/
4+
5+
import type { Meta, StoryObj } from "@storybook/react-vite";
6+
import { storybookLayoutDecorator } from "@storybook-common";
7+
import { useMemo, useState } from "react";
8+
9+
import { Flex } from "@blueprintjs/labs";
10+
11+
import { type HotkeyConfig, useHotkeys, type UseHotkeysOptions } from "../../hooks";
12+
import { Code } from "../html/html";
13+
14+
import { KeyComboTag } from "./keyComboTag";
15+
16+
interface UseHotkeysStoryProps {
17+
showDialogKeyCombo?: UseHotkeysOptions["showDialogKeyCombo"];
18+
}
19+
20+
function UseHotkeysStory({ showDialogKeyCombo }: UseHotkeysStoryProps) {
21+
const [lastFired, setLastFired] = useState<string>();
22+
23+
const hotkeys = useMemo<readonly HotkeyConfig[]>(
24+
() => [
25+
{
26+
combo: "mod + c",
27+
global: true,
28+
group: "Edit",
29+
label: "Copy",
30+
onKeyDown: () => setLastFired("Copy"),
31+
},
32+
{
33+
combo: "mod + v",
34+
global: true,
35+
group: "Edit",
36+
label: "Paste",
37+
onKeyDown: () => setLastFired("Paste"),
38+
},
39+
{
40+
combo: "mod + z",
41+
global: true,
42+
group: "Edit",
43+
label: "Undo",
44+
onKeyDown: () => setLastFired("Undo"),
45+
},
46+
{
47+
combo: "mod + shift + z",
48+
global: true,
49+
group: "Edit",
50+
label: "Redo",
51+
onKeyDown: () => setLastFired("Redo"),
52+
},
53+
],
54+
[],
55+
);
56+
57+
useHotkeys(hotkeys, { showDialogKeyCombo });
58+
59+
return (
60+
<Flex flexDirection="column" gap={4} style={{ width: 320 }}>
61+
<Flex alignItems="center" gap={3}>
62+
<span>Dialog trigger:</span>
63+
{showDialogKeyCombo === false ? (
64+
<KeyComboTag combo="(disabled)" />
65+
) : (
66+
<KeyComboTag combo={showDialogKeyCombo ?? "?"} />
67+
)}
68+
</Flex>
69+
<Flex flexDirection="column" gap={2}>
70+
{hotkeys.map(({ combo, label }) => (
71+
<Flex key={combo} alignItems="center" gap={3}>
72+
<div style={{ minWidth: 140 }}>
73+
<KeyComboTag combo={combo} />
74+
</div>
75+
<span>{label}</span>
76+
</Flex>
77+
))}
78+
</Flex>
79+
<Flex alignItems="center" gap={3}>
80+
<span>Last fired:</span>
81+
<Code>{lastFired ?? "(none)"}</Code>
82+
</Flex>
83+
</Flex>
84+
);
85+
}
86+
87+
const meta: Meta<typeof UseHotkeysStory> = {
88+
title: "Core/Hotkeys/useHotkeys",
89+
component: UseHotkeysStory,
90+
decorators: [storybookLayoutDecorator],
91+
args: {
92+
showDialogKeyCombo: "?",
93+
},
94+
argTypes: {
95+
showDialogKeyCombo: {
96+
description:
97+
"Key combo that opens the built-in help dialog. Pass `false` to disable the trigger entirely; the dialog can still be opened programmatically via `HotkeysContext`.",
98+
control: { type: "radio" },
99+
options: ["?", "shift + h", "Disabled (false)"],
100+
mapping: {
101+
"?": "?",
102+
"shift + h": "shift + h",
103+
"Disabled (false)": false,
104+
},
105+
},
106+
},
107+
} satisfies Meta<typeof UseHotkeysStory>;
108+
109+
export default meta;
110+
type Story = StoryObj<typeof meta>;
111+
112+
/**
113+
* Registers Copy, Paste, Undo, and Redo as global hotkeys. Use the
114+
* `showDialogKeyCombo` control to customize or disable the help dialog trigger.
115+
*/
116+
export const Default: Story = {};

packages/core/src/hooks/hotkeys/useHotkeys.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ export interface UseHotkeysOptions {
3333
document?: Document;
3434

3535
/**
36-
* The key combo which will trigger the hotkeys dialog to open.
36+
* The key combo which will trigger the hotkeys dialog to open. Pass `false`
37+
* to disable the built-in trigger entirely; the dialog can still be opened
38+
* programmatically via {@link HotkeysContext}.
3739
*
3840
* @default "?"
3941
*/
40-
showDialogKeyCombo?: string;
42+
showDialogKeyCombo?: string | false;
4143
}
4244

4345
export interface UseHotkeysReturnValue {
@@ -120,14 +122,16 @@ export function useHotkeys(keys: readonly HotkeyConfig[], options: UseHotkeysOpt
120122

121123
const handleGlobalKeyDown = useCallback(
122124
(e: KeyboardEvent) => {
123-
// special case for global keydown: if '?' is pressed, open the hotkeys dialog
124125
const combo = getKeyCombo(e);
125-
const isTextInput = elementIsTextInput(e.target as HTMLElement);
126-
if (!isTextInput && comboMatches(parseKeyCombo(showDialogKeyCombo), combo)) {
127-
dispatch({ type: "OPEN_DIALOG" });
128-
} else {
129-
invokeNamedCallbackIfComboRecognized(true, getKeyCombo(e), "onKeyDown", e);
126+
// special case for global keydown: if the dialog combo is pressed, open the hotkeys dialog
127+
if (showDialogKeyCombo !== false) {
128+
const isTextInput = elementIsTextInput(e.target as HTMLElement);
129+
if (!isTextInput && comboMatches(parseKeyCombo(showDialogKeyCombo), combo)) {
130+
dispatch({ type: "OPEN_DIALOG" });
131+
return;
132+
}
130133
}
134+
invokeNamedCallbackIfComboRecognized(true, combo, "onKeyDown", e);
131135
},
132136
[dispatch, invokeNamedCallbackIfComboRecognized, showDialogKeyCombo],
133137
);

packages/core/src/hooks/useHotkeys.test.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,16 @@ interface TestComponentProps extends TestComponentContainerProps {
3333
interface TestComponentContainerProps {
3434
bindExtraKeys?: boolean;
3535
isInputReadOnly?: boolean;
36+
showDialogKeyCombo?: string | false;
3637
}
3738

38-
const TestComponent: React.FC<TestComponentProps> = ({ bindExtraKeys, isInputReadOnly, onKeyA, onKeyB }) => {
39+
const TestComponent: React.FC<TestComponentProps> = ({
40+
bindExtraKeys,
41+
isInputReadOnly,
42+
onKeyA,
43+
onKeyB,
44+
showDialogKeyCombo,
45+
}) => {
3946
const hotkeys = useMemo(() => {
4047
const keys = [
4148
{
@@ -68,7 +75,7 @@ const TestComponent: React.FC<TestComponentProps> = ({ bindExtraKeys, isInputRea
6875
return keys;
6976
}, [bindExtraKeys, onKeyA, onKeyB]);
7077

71-
const { handleKeyDown, handleKeyUp } = useHotkeys(hotkeys);
78+
const { handleKeyDown, handleKeyUp } = useHotkeys(hotkeys, { showDialogKeyCombo });
7279

7380
return (
7481
<div onKeyDown={handleKeyDown} onKeyUp={handleKeyUp}>
@@ -195,4 +202,31 @@ describe("useHotkeys", () => {
195202
expect(warnSpy).not.toHaveBeenCalled();
196203
});
197204
});
205+
206+
describe("showDialogKeyCombo", () => {
207+
it("opens the help dialog when the default combo (?) is pressed", async () => {
208+
const user = userEvent.setup();
209+
render(
210+
<HotkeysProvider>
211+
<TestComponentContainer />
212+
</HotkeysProvider>,
213+
);
214+
expect(screen.queryByRole("dialog")).toBeNull();
215+
screen.getByTestId("target-outside-component").focus();
216+
await user.keyboard("{Shift>}/{/Shift}");
217+
expect(await screen.findByRole("dialog")).toBeInTheDocument();
218+
});
219+
220+
it("does not open the help dialog when showDialogKeyCombo is false", async () => {
221+
const user = userEvent.setup();
222+
render(
223+
<HotkeysProvider>
224+
<TestComponentContainer showDialogKeyCombo={false} />
225+
</HotkeysProvider>,
226+
);
227+
screen.getByTestId("target-outside-component").focus();
228+
await user.keyboard("{Shift>}/{/Shift}");
229+
expect(screen.queryByRole("dialog")).toBeNull();
230+
});
231+
});
198232
});

0 commit comments

Comments
 (0)