Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/hooks/src/useKeyPress/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,17 @@ describe('useKeyPress ', () => {
fireEvent.keyUp(document, { key: 'meta', keyCode: 91, metaKey: false });
expect(callback).toBeCalled();
});

it('test callback key', async () => {
let triggerKey;
renderHook(() =>
useKeyPress(['ctrl.uparrow', 'ctrl.meta.c'], (e, key) => {
triggerKey = key;
}),
);
fireEvent.keyDown(document, { key: 'ArrowUp', keyCode: 38, ctrlKey: true });
expect(triggerKey).toBe('ctrl.uparrow');
fireEvent.keyDown(document, { key: 'c', keyCode: 67, ctrlKey: true, metaKey: true });
expect(triggerKey).toBe('ctrl.meta.c');
});
});
42 changes: 42 additions & 0 deletions packages/hooks/src/useKeyPress/demo/demo8.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* title: Get the trigger key
* desc: Multiple shortcuts are registered by a hook, each corresponding to a different logic.
*
* title.zh-CN: 获取触发的按键
* desc.zh-CN: 单个 hook 注册多个快捷键,每个快捷键对应不同逻辑。
*/

import React, { useState } from 'react';
import { useKeyPress } from 'ahooks';

export default () => {
const [count, setCount] = useState<number>(0);

const keyCallbackMap = {
w: () => {
setCount((prev) => prev + 1);
},
s: () => {
setCount((prev) => prev - 1);
},
'shift.c': () => {
setCount(0);
},
};

useKeyPress(['w', 's', 'shift.c'], (e, key) => {
keyCallbackMap[key]();
});

return (
<div>
<p>Try pressing the following: </p>
<div>1. Press [w] to increase</div>
<div>2. Press [s] to decrease</div>
<div>3. Press [shift.c] to reset</div>
<p>
counter: <span style={{ color: '#f00' }}>{count}</span>
</p>
</div>
);
};
16 changes: 10 additions & 6 deletions packages/hooks/src/useKeyPress/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Listen for the keyboard press, support key combinations, and support alias.

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

### Get the trigger key

<code src="./demo/demo8.tsx" />

### Custom method

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

```typescript
type keyType = number | string;
type KeyFilter = keyType | keyType[] | ((event: KeyboardEvent) => boolean);
type KeyType = number | string;
type KeyFilter = KeyType | KeyType[] | ((event: KeyboardEvent) => boolean);

useKeyPress(
keyFilter: KeyFilter,
eventHandler: EventHandler,
eventHandler: (event: KeyboardEvent, key: KeyType) => void,
options?: Options
);
```
Expand All @@ -50,9 +54,9 @@ useKeyPress(

| Property | Description | Type | Default |
| ------------ | ---------------------------------------------------------------- | --------------------------------------------------------------- | ------- |
| keyFilter | Support keyCode、alias、combination keys、array、custom function | `keyType` \| `keyType[]` \| `(event: KeyboardEvent) => boolean` | - |
| eventHandler | Callback function | `(event: KeyboardEvent) => void` | - |
| options | advanced options | `Options` | - |
| keyFilter | Support keyCode、alias、combination keys、array、custom function | `KeyType` \| `KeyType[]` \| `(event: KeyboardEvent) => boolean` | - |
| eventHandler | Callback function | `(event: KeyboardEvent, key: KeyType) => void` | - |
| options | Advanced options | `Options` | - |

### Options

Expand Down
65 changes: 50 additions & 15 deletions packages/hooks/src/useKeyPress/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import { getTargetElement } from '../utils/domTarget';
import useDeepCompareEffectWithTarget from '../utils/useDeepCompareWithTarget';
import isAppleDevice from '../utils/isAppleDevice';

export type KeyPredicate = (event: KeyboardEvent) => boolean;
export type keyType = number | string;
export type KeyFilter = keyType | keyType[] | ((event: KeyboardEvent) => boolean);
export type EventHandler = (event: KeyboardEvent) => void;
export type KeyType = number | string;
export type KeyPredicate = (event: KeyboardEvent) => KeyType | boolean | undefined;
export type KeyFilter = KeyType | KeyType[] | ((event: KeyboardEvent) => boolean);
export type KeyEvent = 'keydown' | 'keyup';

export type Target = BasicTarget<HTMLElement | Document | Window>;
Expand Down Expand Up @@ -137,6 +136,11 @@ const modifierKey = {
},
};

// 判断合法的按键类型
function isValidKeyType(value: unknown): value is string | number {
return isString(value) || isNumber(value);
}

// 根据 event 计算激活键数量
function countKeyByEvent(event: KeyboardEvent) {
const countOfModifier = Object.keys(modifierKey).reduce((total, key) => {
Expand All @@ -155,17 +159,17 @@ function countKeyByEvent(event: KeyboardEvent) {
* 判断按键是否激活
* @param [event: KeyboardEvent]键盘事件
* @param [keyFilter: any] 当前键
* @returns Boolean
* @returns string | number | boolean
*/
function genFilterKey(event: KeyboardEvent, keyFilter: keyType, exactMatch: boolean) {
function genFilterKey(event: KeyboardEvent, keyFilter: KeyType, exactMatch: boolean) {
// 浏览器自动补全 input 的时候,会触发 keyDown、keyUp 事件,但此时 event.key 等为空
if (!event.key) {
return false;
}

// 数字类型直接匹配事件的 keyCode
if (isNumber(keyFilter)) {
return event.keyCode === keyFilter;
return event.keyCode === keyFilter ? keyFilter : false;
}

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

/**
Expand All @@ -204,19 +208,23 @@ function genKeyFormatter(keyFilter: KeyFilter, exactMatch: boolean): KeyPredicat
if (isFunction(keyFilter)) {
return keyFilter;
}
if (isString(keyFilter) || isNumber(keyFilter)) {
if (isValidKeyType(keyFilter)) {
return (event: KeyboardEvent) => genFilterKey(event, keyFilter, exactMatch);
}
if (Array.isArray(keyFilter)) {
return (event: KeyboardEvent) =>
keyFilter.some((item) => genFilterKey(event, item, exactMatch));
keyFilter.find((item) => genFilterKey(event, item, exactMatch));
}
return () => Boolean(keyFilter);
}

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

function useKeyPress(keyFilter: KeyFilter, eventHandler: EventHandler, option?: Options) {
function useKeyPress(
keyFilter: KeyFilter,
eventHandler: (event: KeyboardEvent, key: KeyType) => void,
option?: Options,
) {
const { events = defaultEvents, target, exactMatch = false, useCapture = false } = option || {};
const eventHandlerRef = useLatest(eventHandler);
const keyFilterRef = useLatest(keyFilter);
Expand All @@ -229,9 +237,17 @@ function useKeyPress(keyFilter: KeyFilter, eventHandler: EventHandler, option?:
}

const callbackHandler = (event: KeyboardEvent) => {
const genGuard: KeyPredicate = genKeyFormatter(keyFilterRef.current, exactMatch);
if (genGuard(event)) {
return eventHandlerRef.current?.(event);
const genGuard = genKeyFormatter(keyFilterRef.current, exactMatch);
const keyGuard = genGuard(event);
// When `keyGuard` is not a string or number, this means that we cannot know which key was pressed.
const firedKey = isValidKeyType(keyGuard)
? keyGuard
: exactMatch
? getExactKey(event)
: event.key;

if (keyGuard) {
return eventHandlerRef.current?.(event, firedKey);
}
};

Expand All @@ -249,4 +265,23 @@ function useKeyPress(keyFilter: KeyFilter, eventHandler: EventHandler, option?:
);
}

function getExactKey(event: KeyboardEvent) {
const result: string[] = [];
if (event.altKey) {
result.push('alt');
}
if (event.ctrlKey) {
result.push('ctrl');
}
if (event.metaKey) {
result.push('meta');
}
if (event.shiftKey) {
result.push('shift');
}
const key = event.key.toLowerCase();
if (!result.includes(key)) result.push(key);
return result.join('.');
}

export default useKeyPress;
14 changes: 9 additions & 5 deletions packages/hooks/src/useKeyPress/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ nav:

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

### 获取触发的按键

<code src="./demo/demo8.tsx" />

### 自定义监听方式

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

```typescript
type keyType = number | string;
type KeyFilter = keyType | keyType[] | ((event: KeyboardEvent) => boolean);
type KeyType = number | string;
type KeyFilter = KeyType | KeyType[] | ((event: KeyboardEvent) => boolean);

useKeyPress(
keyFilter: KeyFilter,
eventHandler: EventHandler,
eventHandler: (event: KeyboardEvent, key: KeyType) => void,
options?: Options
);
```
Expand All @@ -50,8 +54,8 @@ useKeyPress(

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

### Options
Expand Down