Skip to content

Commit 8ed956e

Browse files
Simon-He95liuyib
authored andcommitted
feat: useKeyPress callback passes in the triggered shortcut key (alibaba#2170)
* feat: useKeyPress callback passes in the triggered shortcut key * chore: update md * chore: update * fix: test * chore: update md * chore: update md * chore: update * style: change name to pascal * docs: fix type in eventHandler * style: rename 'code' to 'key' * chore: add new demo * test: rename & remove useless option * refactor: 当 keyFilter 传函数时保持原来的用法 * fix: can't pass ci when keyFilter is array * fix: second callback is not be called * chore: firedKey when functional keyFilter * chore: update * test: better case * test: update * test: remove useless * refactor: keyFilter 传函数的情况,不用再处理 exactMatch --------- Co-authored-by: liuyib <[email protected]>
1 parent ff93274 commit 8ed956e

File tree

5 files changed

+138
-26
lines changed

5 files changed

+138
-26
lines changed

packages/hooks/src/useKeyPress/__tests__/index.test.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,55 @@ describe('useKeyPress ', () => {
8383
fireEvent.keyUp(document, { key: 'meta', keyCode: 91, metaKey: false });
8484
expect(callback).toBeCalled();
8585
});
86+
87+
it('test `keyFilter` function parameter', async () => {
88+
const callback1 = jest.fn();
89+
const callback2 = jest.fn();
90+
91+
// all keys can trigger callback
92+
const hook1 = renderHook(() => useKeyPress(() => true, callback1));
93+
fireEvent.keyDown(document, { key: '0', keyCode: 48 });
94+
fireEvent.keyDown(document, { key: 'a', keyCode: 65 });
95+
expect(callback1.mock.calls.length).toBe(2);
96+
97+
// only some keys can trigger callback
98+
const hook2 = renderHook(() => useKeyPress((e) => ['0', 'meta'].includes(e.key), callback2));
99+
fireEvent.keyDown(document, { key: '0', keyCode: 48 });
100+
fireEvent.keyDown(document, { key: '1', keyCode: 49 });
101+
fireEvent.keyDown(document, { key: 'ctrl', keyCode: 17, ctrlKey: true });
102+
fireEvent.keyDown(document, { key: 'meta', keyCode: 91, metaKey: true });
103+
expect(callback2.mock.calls.length).toBe(2);
104+
105+
hook1.unmount();
106+
hook2.unmount();
107+
});
108+
109+
it('test key in `eventHandler` parameter', async () => {
110+
let pressedKey;
111+
const KEYS = ['c', 'shift.c', 'shift.ctrl.c'];
112+
const callbackKey = (e, key) => {
113+
pressedKey = key;
114+
};
115+
116+
// test `exactMatch: true` props
117+
const hook1 = renderHook(() => useKeyPress(KEYS, callbackKey, { exactMatch: true }));
118+
fireEvent.keyDown(document, { key: 'c', keyCode: 67 });
119+
expect(pressedKey).toBe('c');
120+
fireEvent.keyDown(document, { key: 'c', keyCode: 67, shiftKey: true });
121+
expect(pressedKey).toBe('shift.c');
122+
fireEvent.keyDown(document, { key: 'c', keyCode: 67, shiftKey: true, ctrlKey: true });
123+
expect(pressedKey).toBe('shift.ctrl.c');
124+
125+
// test `exactMatch: false`(default) props
126+
const hook2 = renderHook(() => useKeyPress(KEYS, callbackKey));
127+
fireEvent.keyDown(document, { key: 'c', keyCode: 67 });
128+
expect(pressedKey).toBe('c');
129+
fireEvent.keyDown(document, { key: 'c', keyCode: 67, shiftKey: true });
130+
expect(pressedKey).toBe('c');
131+
fireEvent.keyDown(document, { key: 'c', keyCode: 67, shiftKey: true, ctrlKey: true });
132+
expect(pressedKey).toBe('c');
133+
134+
hook2.unmount();
135+
hook1.unmount();
136+
});
86137
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* title: Get the trigger key
3+
* desc: Multiple shortcuts are registered by a hook, each corresponding to a different logic.
4+
*
5+
* title.zh-CN: 获取触发的按键
6+
* desc.zh-CN: 单个 hook 注册多个快捷键,每个快捷键对应不同逻辑。
7+
*/
8+
9+
import React, { useState } from 'react';
10+
import { useKeyPress } from 'ahooks';
11+
12+
export default () => {
13+
const [count, setCount] = useState<number>(0);
14+
15+
const keyCallbackMap = {
16+
w: () => {
17+
setCount((prev) => prev + 1);
18+
},
19+
s: () => {
20+
setCount((prev) => prev - 1);
21+
},
22+
'shift.c': () => {
23+
setCount(0);
24+
},
25+
};
26+
27+
useKeyPress(['w', 's', 'shift.c'], (e, key) => {
28+
keyCallbackMap[key]();
29+
});
30+
31+
return (
32+
<div>
33+
<p>Try pressing the following: </p>
34+
<div>1. Press [w] to increase</div>
35+
<div>2. Press [s] to decrease</div>
36+
<div>3. Press [shift.c] to reset</div>
37+
<p>
38+
counter: <span style={{ color: '#f00' }}>{count}</span>
39+
</p>
40+
</div>
41+
);
42+
};

packages/hooks/src/useKeyPress/index.en-US.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ Listen for the keyboard press, support key combinations, and support alias.
2525

2626
<code src="./demo/demo3.tsx" />
2727

28+
### Get the trigger key
29+
30+
<code src="./demo/demo8.tsx" />
31+
2832
### Custom method
2933

3034
<code src="./demo/demo4.tsx" />
@@ -36,12 +40,12 @@ Listen for the keyboard press, support key combinations, and support alias.
3640
## API
3741

3842
```typescript
39-
type keyType = number | string;
40-
type KeyFilter = keyType | keyType[] | ((event: KeyboardEvent) => boolean);
43+
type KeyType = number | string;
44+
type KeyFilter = KeyType | KeyType[] | ((event: KeyboardEvent) => boolean);
4145

4246
useKeyPress(
4347
keyFilter: KeyFilter,
44-
eventHandler: EventHandler,
48+
eventHandler: (event: KeyboardEvent, key: KeyType) => void,
4549
options?: Options
4650
);
4751
```
@@ -50,9 +54,9 @@ useKeyPress(
5054

5155
| Property | Description | Type | Default |
5256
| ------------ | ---------------------------------------------------------------- | --------------------------------------------------------------- | ------- |
53-
| keyFilter | Support keyCode、alias、combination keys、array、custom function | `keyType` \| `keyType[]` \| `(event: KeyboardEvent) => boolean` | - |
54-
| eventHandler | Callback function | `(event: KeyboardEvent) => void` | - |
55-
| options | advanced options | `Options` | - |
57+
| keyFilter | Support keyCode、alias、combination keys、array、custom function | `KeyType` \| `KeyType[]` \| `(event: KeyboardEvent) => boolean` | - |
58+
| eventHandler | Callback function | `(event: KeyboardEvent, key: KeyType) => void` | - |
59+
| options | Advanced options | `Options` | - |
5660

5761
### Options
5862

packages/hooks/src/useKeyPress/index.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ import { getTargetElement } from '../utils/domTarget';
55
import useDeepCompareEffectWithTarget from '../utils/useDeepCompareWithTarget';
66
import isAppleDevice from '../utils/isAppleDevice';
77

8-
export type KeyPredicate = (event: KeyboardEvent) => boolean;
9-
export type keyType = number | string;
10-
export type KeyFilter = keyType | keyType[] | ((event: KeyboardEvent) => boolean);
11-
export type EventHandler = (event: KeyboardEvent) => void;
8+
export type KeyType = number | string;
9+
export type KeyPredicate = (event: KeyboardEvent) => KeyType | boolean | undefined;
10+
export type KeyFilter = KeyType | KeyType[] | ((event: KeyboardEvent) => boolean);
1211
export type KeyEvent = 'keydown' | 'keyup';
1312

1413
export type Target = BasicTarget<HTMLElement | Document | Window>;
@@ -137,6 +136,11 @@ const modifierKey = {
137136
},
138137
};
139138

139+
// 判断合法的按键类型
140+
function isValidKeyType(value: unknown): value is string | number {
141+
return isString(value) || isNumber(value);
142+
}
143+
140144
// 根据 event 计算激活键数量
141145
function countKeyByEvent(event: KeyboardEvent) {
142146
const countOfModifier = Object.keys(modifierKey).reduce((total, key) => {
@@ -155,17 +159,17 @@ function countKeyByEvent(event: KeyboardEvent) {
155159
* 判断按键是否激活
156160
* @param [event: KeyboardEvent]键盘事件
157161
* @param [keyFilter: any] 当前键
158-
* @returns Boolean
162+
* @returns string | number | boolean
159163
*/
160-
function genFilterKey(event: KeyboardEvent, keyFilter: keyType, exactMatch: boolean) {
164+
function genFilterKey(event: KeyboardEvent, keyFilter: KeyType, exactMatch: boolean) {
161165
// 浏览器自动补全 input 的时候,会触发 keyDown、keyUp 事件,但此时 event.key 等为空
162166
if (!event.key) {
163167
return false;
164168
}
165169

166170
// 数字类型直接匹配事件的 keyCode
167171
if (isNumber(keyFilter)) {
168-
return event.keyCode === keyFilter;
172+
return event.keyCode === keyFilter ? keyFilter : false;
169173
}
170174

171175
// 字符串依次判断是否有组合键
@@ -190,9 +194,9 @@ function genFilterKey(event: KeyboardEvent, keyFilter: keyType, exactMatch: bool
190194
* 主要用来防止按组合键其子集也会触发的情况,例如监听 ctrl+a 会触发监听 ctrl 和 a 两个键的事件。
191195
*/
192196
if (exactMatch) {
193-
return genLen === genArr.length && countKeyByEvent(event) === genArr.length;
197+
return genLen === genArr.length && countKeyByEvent(event) === genArr.length ? keyFilter : false;
194198
}
195-
return genLen === genArr.length;
199+
return genLen === genArr.length ? keyFilter : false;
196200
}
197201

198202
/**
@@ -204,19 +208,23 @@ function genKeyFormatter(keyFilter: KeyFilter, exactMatch: boolean): KeyPredicat
204208
if (isFunction(keyFilter)) {
205209
return keyFilter;
206210
}
207-
if (isString(keyFilter) || isNumber(keyFilter)) {
211+
if (isValidKeyType(keyFilter)) {
208212
return (event: KeyboardEvent) => genFilterKey(event, keyFilter, exactMatch);
209213
}
210214
if (Array.isArray(keyFilter)) {
211215
return (event: KeyboardEvent) =>
212-
keyFilter.some((item) => genFilterKey(event, item, exactMatch));
216+
keyFilter.find((item) => genFilterKey(event, item, exactMatch));
213217
}
214218
return () => Boolean(keyFilter);
215219
}
216220

217221
const defaultEvents: KeyEvent[] = ['keydown'];
218222

219-
function useKeyPress(keyFilter: KeyFilter, eventHandler: EventHandler, option?: Options) {
223+
function useKeyPress(
224+
keyFilter: KeyFilter,
225+
eventHandler: (event: KeyboardEvent, key: KeyType) => void,
226+
option?: Options,
227+
) {
220228
const { events = defaultEvents, target, exactMatch = false, useCapture = false } = option || {};
221229
const eventHandlerRef = useLatest(eventHandler);
222230
const keyFilterRef = useLatest(keyFilter);
@@ -229,9 +237,12 @@ function useKeyPress(keyFilter: KeyFilter, eventHandler: EventHandler, option?:
229237
}
230238

231239
const callbackHandler = (event: KeyboardEvent) => {
232-
const genGuard: KeyPredicate = genKeyFormatter(keyFilterRef.current, exactMatch);
233-
if (genGuard(event)) {
234-
return eventHandlerRef.current?.(event);
240+
const genGuard = genKeyFormatter(keyFilterRef.current, exactMatch);
241+
const keyGuard = genGuard(event);
242+
const firedKey = isValidKeyType(keyGuard) ? keyGuard : event.key;
243+
244+
if (keyGuard) {
245+
return eventHandlerRef.current?.(event, firedKey);
235246
}
236247
};
237248

packages/hooks/src/useKeyPress/index.zh-CN.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ nav:
2525

2626
<code src="./demo/demo3.tsx" />
2727

28+
### 获取触发的按键
29+
30+
<code src="./demo/demo8.tsx" />
31+
2832
### 自定义监听方式
2933

3034
<code src="./demo/demo4.tsx" />
@@ -36,12 +40,12 @@ nav:
3640
## API
3741

3842
```typescript
39-
type keyType = number | string;
40-
type KeyFilter = keyType | keyType[] | ((event: KeyboardEvent) => boolean);
43+
type KeyType = number | string;
44+
type KeyFilter = KeyType | KeyType[] | ((event: KeyboardEvent) => boolean);
4145

4246
useKeyPress(
4347
keyFilter: KeyFilter,
44-
eventHandler: EventHandler,
48+
eventHandler: (event: KeyboardEvent, key: KeyType) => void,
4549
options?: Options
4650
);
4751
```
@@ -50,8 +54,8 @@ useKeyPress(
5054

5155
| 参数 | 说明 | 类型 | 默认值 |
5256
| ------------ | -------------------------------------------- | --------------------------------------------------------------- | ------ |
53-
| keyFilter | 支持 keyCode、别名、组合键、数组自定义函数 | `keyType` \| `keyType[]` \| `(event: KeyboardEvent) => boolean` | - |
54-
| eventHandler | 回调函数 | `(event: KeyboardEvent) => void` | - |
57+
| keyFilter | 支持 keyCode、别名、组合键、数组自定义函数 | `KeyType` \| `KeyType[]` \| `(event: KeyboardEvent) => boolean` | - |
58+
| eventHandler | 回调函数 | `(event: KeyboardEvent, key: KeyType) => void` | - |
5559
| options | 可选配置项 | `Options` | - |
5660

5761
### Options

0 commit comments

Comments
 (0)