Skip to content

Commit 92b32ee

Browse files
authored
Merge pull request #527 from easyops-cn/copilot/fix-0fb12dbc-763e-4e6c-99eb-f49ee2f14f74
feat: Add configurable keyboard shortcut for search bar focus closes #516
2 parents 92a1001 + a3d2a74 commit 92b32ee

File tree

12 files changed

+499
-15
lines changed

12 files changed

+499
-15
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ module.exports = {
5959
// For Docs using Chinese, it is recomended to set:
6060
// language: ["en", "zh"],
6161

62+
// Customize the keyboard shortcut to focus search bar (default is "mod+k"):
63+
// searchBarShortcutKeymap: "s", // Use 'S' key
64+
// searchBarShortcutKeymap: "ctrl+shift+f", // Use Ctrl+Shift+F
65+
6266
// If you're using `noIndex: true`, set `forceIgnoreNoIndex` to enable local index:
6367
// forceIgnoreNoIndex: true,
6468
}),
@@ -92,6 +96,7 @@ module.exports = {
9296
| ignoreCssSelectors | string \| string[] | `[]` | A list of css selectors to ignore when indexing each page. |
9397
| searchBarShortcut | boolean | `true` | Whether to enable keyboard shortcut to focus in search bar. |
9498
| searchBarShortcutHint | boolean | `true` | Whether to show keyboard shortcut hint in search bar. Disable it if you need to hide the hint while shortcut is still enabled. |
99+
| searchBarShortcutKeymap | string | `"mod+k"` | Custom keyboard shortcut to focus the search bar. Supports formats like: `"s"` for single key, `"ctrl+k"` for key combinations, `"mod+k"` for Command+K (Mac) / Ctrl+K (others) - recommended cross-platform option, `"ctrl+shift+k"` for multiple modifiers. |
95100
| searchBarPosition | `"auto"` \| `"left"` \| `"right"` | `"auto"` | The side of the navbar the search bar should appear on. By default, it will try to autodetect based on your docusaurus config according to [the docs](https://docusaurus.io/docs/api/themes/configuration#navbar-search). |
96101
| docsPluginIdForPreferredVersion | string | | When you're using multi-instance of docs, set the docs plugin id which you'd like to check the preferred version with, for the search index. |
97102
| zhUserDict | string | | Provide your custom dict for language of zh, [see here](https://github.com/fxsjy/jieba#%E8%BD%BD%E5%85%A5%E8%AF%8D%E5%85%B8) |

docusaurus-search-local/src/client/theme/SearchBar/SearchBar.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
Mark,
2525
searchBarShortcut,
2626
searchBarShortcutHint,
27+
searchBarShortcutKeymap,
2728
searchBarPosition,
2829
docsPluginIdForPreferredVersion,
2930
indexDocs,
@@ -34,6 +35,8 @@ import {
3435
import LoadingRing from "../LoadingRing/LoadingRing";
3536
import { normalizeContextByPath } from "../../utils/normalizeContextByPath";
3637
import { searchResultLimits } from "../../utils/proxiedGeneratedConstants";
38+
import { parseKeymap, matchesKeymap, getKeymapHints } from "../../utils/keymap";
39+
import { isMacPlatform } from "../../utils/platform";
3740

3841
import styles from "./SearchBar.module.css";
3942

@@ -387,11 +390,7 @@ export default function SearchBar({
387390
);
388391

389392
// Implement hint icons for the search shortcuts on mac and the rest operating systems.
390-
const isMac = isBrowser
391-
? /mac/i.test(
392-
(navigator as any).userAgentData?.platform ?? navigator.platform
393-
)
394-
: false;
393+
const isMac = isBrowser ? isMacPlatform() : false;
395394

396395
// Sync the input value and focus state for SSR
397396
useEffect(
@@ -414,15 +413,15 @@ export default function SearchBar({
414413
);
415414

416415
useEffect(() => {
417-
if (!searchBarShortcut) {
416+
if (!searchBarShortcut || !searchBarShortcutKeymap) {
418417
return;
419418
}
420-
// Add shortcuts command/ctrl + K
419+
420+
const parsedKeymap = parseKeymap(searchBarShortcutKeymap);
421+
422+
// Add shortcuts based on custom keymap
421423
const handleShortcut = (event: KeyboardEvent): void => {
422-
if (
423-
(isMac ? event.metaKey : event.ctrlKey) &&
424-
(event.key === "k" || event.key === "K")
425-
) {
424+
if (matchesKeymap(event, parsedKeymap)) {
426425
event.preventDefault();
427426
searchBarRef.current?.focus();
428427
onInputFocus();
@@ -433,7 +432,7 @@ export default function SearchBar({
433432
return () => {
434433
document.removeEventListener("keydown", handleShortcut);
435434
};
436-
}, [isMac, onInputFocus]);
435+
}, [onInputFocus, searchBarShortcutKeymap]);
437436

438437
const onClearSearch = useCallback(() => {
439438
const params = new URLSearchParams(location.search);
@@ -485,10 +484,11 @@ export default function SearchBar({
485484
486485
</button>
487486
) : (
488-
isBrowser && (
487+
isBrowser && searchBarShortcutKeymap && (
489488
<div className={styles.searchHintContainer}>
490-
<kbd className={styles.searchHint}>{isMac ? "⌘" : "ctrl"}</kbd>
491-
<kbd className={styles.searchHint}>K</kbd>
489+
{getKeymapHints(searchBarShortcutKeymap, isMac).map((hint, index) => (
490+
<kbd key={index} className={styles.searchHint}>{hint}</kbd>
491+
))}
492492
</div>
493493
)
494494
))}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { parseKeymap, matchesKeymap, getKeymapHints } from './keymap';
2+
import * as platformModule from './platform';
3+
4+
// Mock the platform module
5+
jest.mock('./platform');
6+
7+
describe('keymap utility functions', () => {
8+
const mockIsMacPlatform = jest.mocked(platformModule.isMacPlatform);
9+
10+
beforeEach(() => {
11+
mockIsMacPlatform.mockClear();
12+
});
13+
describe('parseKeymap', () => {
14+
test('should parse single key', () => {
15+
const result = parseKeymap('s');
16+
expect(result).toEqual({
17+
key: 's',
18+
ctrl: false,
19+
alt: false,
20+
shift: false,
21+
meta: false,
22+
});
23+
});
24+
25+
test('should parse ctrl+k', () => {
26+
const result = parseKeymap('ctrl+k');
27+
expect(result).toEqual({
28+
key: 'k',
29+
ctrl: true,
30+
alt: false,
31+
shift: false,
32+
meta: false,
33+
});
34+
});
35+
36+
test('should parse cmd+k', () => {
37+
const result = parseKeymap('cmd+k');
38+
expect(result).toEqual({
39+
key: 'k',
40+
ctrl: false,
41+
alt: false,
42+
shift: false,
43+
meta: true,
44+
});
45+
});
46+
47+
test('should parse mod+k on Mac-like platform', () => {
48+
mockIsMacPlatform.mockReturnValue(true);
49+
50+
const result = parseKeymap('mod+k');
51+
expect(result).toEqual({
52+
key: 'k',
53+
ctrl: false,
54+
alt: false,
55+
shift: false,
56+
meta: true,
57+
});
58+
});
59+
60+
test('should parse mod+k on non-Mac platform', () => {
61+
mockIsMacPlatform.mockReturnValue(false);
62+
63+
const result = parseKeymap('mod+k');
64+
expect(result).toEqual({
65+
key: 'k',
66+
ctrl: true,
67+
alt: false,
68+
shift: false,
69+
meta: false,
70+
});
71+
});
72+
73+
test('should parse complex combination', () => {
74+
const result = parseKeymap('ctrl+shift+alt+f');
75+
expect(result).toEqual({
76+
key: 'f',
77+
ctrl: true,
78+
alt: true,
79+
shift: true,
80+
meta: false,
81+
});
82+
});
83+
84+
test('should handle whitespace', () => {
85+
const result = parseKeymap(' ctrl + k ');
86+
expect(result).toEqual({
87+
key: 'k',
88+
ctrl: true,
89+
alt: false,
90+
shift: false,
91+
meta: false,
92+
});
93+
});
94+
});
95+
96+
describe('matchesKeymap', () => {
97+
test('should match single key', () => {
98+
const keymap = parseKeymap('s');
99+
const event = {
100+
key: 's',
101+
ctrlKey: false,
102+
altKey: false,
103+
shiftKey: false,
104+
metaKey: false,
105+
} as KeyboardEvent;
106+
107+
expect(matchesKeymap(event, keymap)).toBe(true);
108+
});
109+
110+
test('should match ctrl+k', () => {
111+
const keymap = parseKeymap('ctrl+k');
112+
const event = {
113+
key: 'k',
114+
ctrlKey: true,
115+
altKey: false,
116+
shiftKey: false,
117+
metaKey: false,
118+
} as KeyboardEvent;
119+
120+
expect(matchesKeymap(event, keymap)).toBe(true);
121+
});
122+
123+
test('should not match if modifiers differ', () => {
124+
const keymap = parseKeymap('ctrl+k');
125+
const event = {
126+
key: 'k',
127+
ctrlKey: false,
128+
altKey: false,
129+
shiftKey: false,
130+
metaKey: false,
131+
} as KeyboardEvent;
132+
133+
expect(matchesKeymap(event, keymap)).toBe(false);
134+
});
135+
136+
test('should handle case insensitivity', () => {
137+
const keymap = parseKeymap('ctrl+k');
138+
const event = {
139+
key: 'K',
140+
ctrlKey: true,
141+
altKey: false,
142+
shiftKey: false,
143+
metaKey: false,
144+
} as KeyboardEvent;
145+
146+
expect(matchesKeymap(event, keymap)).toBe(true);
147+
});
148+
});
149+
150+
describe('getKeymapHints', () => {
151+
test('should generate hints for single key', () => {
152+
const hints = getKeymapHints('s', false);
153+
expect(hints).toEqual(['S']);
154+
});
155+
156+
test('should generate hints for ctrl+k on non-Mac', () => {
157+
const hints = getKeymapHints('ctrl+k', false);
158+
expect(hints).toEqual(['ctrl', 'K']);
159+
});
160+
161+
test('should generate hints for cmd+k on Mac', () => {
162+
const hints = getKeymapHints('cmd+k', true);
163+
expect(hints).toEqual(['⌘', 'K']);
164+
});
165+
166+
test('should generate hints for mod+k on Mac', () => {
167+
const hints = getKeymapHints('mod+k', true);
168+
expect(hints).toEqual(['⌘', 'K']);
169+
});
170+
171+
test('should generate hints for mod+k on non-Mac', () => {
172+
const hints = getKeymapHints('mod+k', false);
173+
expect(hints).toEqual(['ctrl', 'K']);
174+
});
175+
176+
test('should generate hints for complex combination on Mac', () => {
177+
const hints = getKeymapHints('ctrl+shift+alt+f', true);
178+
expect(hints).toEqual(['ctrl', '⌥', '⇧', 'F']);
179+
});
180+
181+
test('should generate hints for complex combination on non-Mac', () => {
182+
const hints = getKeymapHints('ctrl+shift+alt+f', false);
183+
expect(hints).toEqual(['ctrl', 'alt', 'shift', 'F']);
184+
});
185+
});
186+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { isMacPlatform } from './platform';
2+
3+
export interface ParsedKeymap {
4+
key: string;
5+
ctrl: boolean;
6+
alt: boolean;
7+
shift: boolean;
8+
meta: boolean;
9+
}
10+
11+
export function parseKeymap(keymap: string): ParsedKeymap {
12+
const parts = keymap.toLowerCase().split('+');
13+
const result: ParsedKeymap = {
14+
key: '',
15+
ctrl: false,
16+
alt: false,
17+
shift: false,
18+
meta: false,
19+
};
20+
21+
// Detect if we're on Mac to handle 'mod' appropriately
22+
const isMac = isMacPlatform();
23+
24+
for (const part of parts) {
25+
const trimmed = part.trim();
26+
switch (trimmed) {
27+
case 'ctrl':
28+
result.ctrl = true;
29+
break;
30+
case 'cmd':
31+
result.meta = true;
32+
break;
33+
case 'mod':
34+
if (isMac) {
35+
result.meta = true;
36+
} else {
37+
result.ctrl = true;
38+
}
39+
break;
40+
case 'alt':
41+
result.alt = true;
42+
break;
43+
case 'shift':
44+
result.shift = true;
45+
break;
46+
default:
47+
result.key = trimmed;
48+
break;
49+
}
50+
}
51+
52+
return result;
53+
}
54+
55+
export function matchesKeymap(event: KeyboardEvent, keymap: ParsedKeymap): boolean {
56+
return (
57+
event.key.toLowerCase() === keymap.key &&
58+
event.ctrlKey === keymap.ctrl &&
59+
event.altKey === keymap.alt &&
60+
event.shiftKey === keymap.shift &&
61+
event.metaKey === keymap.meta
62+
);
63+
}
64+
65+
export function getKeymapHints(keymap: string, isMac: boolean): string[] {
66+
const parsedKeymap = parseKeymap(keymap);
67+
const hints: string[] = [];
68+
69+
// Handle original keymap string to detect 'mod' for proper hint display
70+
const parts = keymap.toLowerCase().split('+').map(p => p.trim());
71+
const hasMod = parts.includes('mod');
72+
73+
if (parsedKeymap.ctrl && !hasMod) {
74+
hints.push('ctrl');
75+
}
76+
if (parsedKeymap.meta && !hasMod) {
77+
hints.push(isMac ? '⌘' : 'cmd');
78+
}
79+
if (hasMod) {
80+
hints.push(isMac ? '⌘' : 'ctrl');
81+
}
82+
if (parsedKeymap.alt) {
83+
hints.push(isMac ? '⌥' : 'alt');
84+
}
85+
if (parsedKeymap.shift) {
86+
hints.push(isMac ? '⇧' : 'shift');
87+
}
88+
if (parsedKeymap.key) {
89+
hints.push(parsedKeymap.key.toUpperCase());
90+
}
91+
92+
return hints;
93+
}

0 commit comments

Comments
 (0)