Skip to content

Commit e1b7ff2

Browse files
committed
Allow passing ref to Keybinding
1 parent a1c455c commit e1b7ff2

File tree

2 files changed

+27
-5
lines changed

2 files changed

+27
-5
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ Get the eventKey keyCode and log it on `keyUp` on the window element
2222
<KeyBinding onKey={ (e) => { console.log(e.keyCode) } } type='keyup' target={ window } />
2323
```
2424

25+
Scope the listener to a container element via a React ref (only fires when focus is inside the container)
26+
``` JSX
27+
const containerRef = useRef(null);
28+
29+
<div ref={containerRef}>
30+
<KeyBinding onKey={ (e) => { console.log(e.keyCode) } } target={ containerRef } />
31+
</div>
32+
```
33+
2534
Have a look at options.
2635

2736
### Options
@@ -32,7 +41,7 @@ All properties except `onKey` are optional.
3241
|----------------------------------|----------------------------------------------------------------------------------------------------------------------|---------------|
3342
| `onKey` (required) | the function executed after a key event | n/a |
3443
| `type` | keyup or keydown | `'keydown'` |
35-
| `target` | the element you want to attach the event to, it can be an **existing** DOM element or a CSS selector (in that case, you will need to add a `tabIndex='0'` to your element, otherwise the event won't be caught) | `document` |
44+
| `target` | the element you want to attach the event to: an **existing** DOM element, a CSS selector (you will need to add `tabIndex='0'` to your element, otherwise the event won't be caught), or a **React ref** (`RefObject<HTMLElement>`) — if a ref is passed and `.current` is `null`, the listener is silently skipped | `document` |
3645
| `preventInputConflict` | prevent onKey from firing if you have an onChange on an input, a textarea or a select | `false` |
3746
| `preventContentEditableConflict` | prevent onKey from firing if the user is editing the DOM via contenteditable="true", usually used by WYSIWYG editors | `false` |
3847
| `preventDefault` | prevent event default | `false` |

src/keybinding.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { useCallback, useEffect } from 'react';
1+
import { type RefObject, useCallback, useEffect } from 'react';
22

33
export type KeybindingProps = {
44
onKey: (e: KeyboardEvent) => void;
55
type?: 'keydown' | 'keyup';
6-
target?: string | HTMLElement | Document | Window;
6+
target?: string | HTMLElement | Document | Window | RefObject<HTMLElement>;
77
preventInputConflict?: boolean;
88
preventContentEditableConflict?: boolean;
99
preventDefault?: boolean;
@@ -12,12 +12,19 @@ export type KeybindingProps = {
1212

1313
const TARGETS_BLACKLIST = ['textarea', 'input', 'select'];
1414

15+
function isRefObject(
16+
target: NonNullable<KeybindingProps['target']>,
17+
): target is RefObject<HTMLElement> {
18+
return typeof target === 'object' && 'current' in target;
19+
}
20+
1521
/**
16-
* Get the actual target to which we should attach the event
22+
* Get the actual target to which we should attach the event.
23+
* Returns null if a ref is passed but not yet mounted.
1724
*/
1825
function getTarget(
1926
target: NonNullable<KeybindingProps['target']>,
20-
): HTMLElement | Document | Window {
27+
): HTMLElement | Document | Window | null {
2128
if (typeof target === 'string') {
2229
const element = document.querySelector<HTMLElement>(target);
2330
if (!element) {
@@ -29,6 +36,10 @@ function getTarget(
2936
return element;
3037
}
3138

39+
if (isRefObject(target)) {
40+
return target.current;
41+
}
42+
3243
return target;
3344
}
3445

@@ -83,6 +94,8 @@ export default function Keybinding(props: KeybindingProps) {
8394
useEffect(() => {
8495
const actualTarget = getTarget(target);
8596

97+
if (!actualTarget) return;
98+
8699
actualTarget.addEventListener(type, onKeyEvent as EventListener);
87100

88101
return () => {

0 commit comments

Comments
 (0)