Skip to content

Commit

Permalink
refactor(useKeyboardEvent): improve the code and change signature (#248)
Browse files Browse the repository at this point in the history
Also improve docs and arguments description.

BREAKING CHANGE: hook call signature has changed.
  • Loading branch information
xobotyi authored Aug 6, 2021
1 parent 25c8599 commit a0e1b24
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 59 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,5 @@ Coming from `react-use`? Check out our [migration guide](https://react-hookz.git
— Sets title of the page.
- [**`useEventListener`**](https://react-hookz.github.io/web/?path=/docs/dom-useeventlistener)
— Subscribes an event listener to the target, and automatically unsubscribes it on unmount.
- [**`useKeyboardEvent`**](https://react-hookz.github.io/web/?path=/docs/dom-usekeyboardevent)
— Executes callback when keyboard event occurred on target.
16 changes: 9 additions & 7 deletions src/useKeyboardEvent/__docs__/example.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,26 @@ import { useKeyboardEvent } from '../..';

export const Example: React.FC = () => {
const [list, setList] = useState<string[]>([]);

useKeyboardEvent(
() => true,
true,
(ev) => {
list.unshift(ev.key);
setList([...list]);
setList((l) => l.slice(-10).concat([ev.key]));
},
[],
{ event: 'keydown', eventOptions: { passive: true } }
{ eventOptions: { passive: true } }
);

return (
<div>
<div>Press any keyboard keys and they will appear below.</div>

<p>You have pressed</p>
<ul>
{list.map((k, idx) => (
{list.map((k, i) => (
// eslint-disable-next-line react/no-array-index-key
<li key={idx}>{k}</li>
))}
<li key={`${i}_${k}`}>{k}</li>
))}{' '}
</ul>
</div>
);
Expand Down
21 changes: 12 additions & 9 deletions src/useKeyboardEvent/__docs__/story.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Example } from './example.stories';

# useKeyboardEvent

React UI sensor hook that executes a handler when a keyboard key is used.
Executes callback when keyboard event occurred on target (window by default).

#### Example

Expand All @@ -17,16 +17,19 @@ React UI sensor hook that executes a handler when a keyboard key is used.

```ts
export function useKeyboardEvent<T extends EventTarget>(
keyOrPredicate: IKeyFilter,
callback: IHandler,
deps: DependencyList = keyOrPredicate,
options: IUseKeyOptions<T>
keyOrPredicate: IKeyboardEventFilter,
callback: IKeyboardEventHandler<T>,
deps?: DependencyList,
options: IUseKeyboardEventOptions<T> = {}
): void;
```

#### Arguments

- **keyOrPredicate** _`IKeyFilter`_ - Key filter can be `string` or callback function accept a KeyboardEvent and return a boolean.
- **callback** listener in `addEventListener`.
- **deps** DependencyList.
- **options** some options passed to addEventListener, event can be `[keydown, keyup, keypress]`, target is the event target default `window`, eventOptions is the third parameter of `addEventListener`.
- **keyOrPredicate** _`IKeyboardEventFilter`_ - Filters keypresses on which `callback` will be executed.
- **callback** _`IKeyboardEventHandler`_ - Function to call when key is pressed and `keyOrPredicate` matches positive.
- **deps** _`DependencyList`_ - Dependencies list that will be passed to underlying `useMemo`.
- **options** - Hook options:
- **event** _`'keydown' | 'keypress' | 'keyup'`_ (default: `keydown`) - Event name that triggers handler.
- **target** _`RefObject<T> | T | null`_ (default: `window`) - Target that should emit event.
- **eventOptions** _`boolean | AddEventListenerOptions`_ - Options that will be passed to underlying `useEventListener` hook.
12 changes: 2 additions & 10 deletions src/useKeyboardEvent/__tests__/dom.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
import { renderHook } from '@testing-library/react-hooks/dom';
import { useKeyboardEvent, IKeyboardEventFilter } from '../..';
import { IKeyboardEventFilter, useKeyboardEvent } from '../..';

describe('useKeyboardEvent', () => {
it('should be defined', () => {
expect(useKeyboardEvent).toBeDefined();
});

it('should render', () => {
const { result } = renderHook(() =>
useKeyboardEvent(
() => true,
() => {}
)
);
const { result } = renderHook(() => useKeyboardEvent('a', () => {}));
expect(result.error).toBeUndefined();

const { result: noopResult } = renderHook(() => useKeyboardEvent(() => true));
expect(noopResult.error).toBeUndefined();
});

it('should bind listener on mount and unbind on unmount', () => {
Expand Down
9 changes: 1 addition & 8 deletions src/useKeyboardEvent/__tests__/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,7 @@ describe('useKeyboardEvent', () => {
});

it('should render', () => {
const { result } = renderHook(() =>
useKeyboardEvent(
() => true,
() => {},
undefined,
{ eventOptions: { passive: true } }
)
);
const { result } = renderHook(() => useKeyboardEvent('a', () => {}));
expect(result.error).toBeUndefined();
});
});
62 changes: 37 additions & 25 deletions src/useKeyboardEvent/useKeyboardEvent.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,64 @@
import { DependencyList, RefObject, useMemo } from 'react';
import { useEventListener } from '..';
import { noop, isBrowser } from '../util/const';
import { yieldTrue, yieldFalse } from '../util/misc';
import { useEventListener, useSyncedRef } from '..';
import { isBrowser } from '../util/const';
import { yieldFalse, yieldTrue } from '../util/misc';

export type IKeyboardEventPredicate = (event: KeyboardEvent) => boolean;
export type IKeyboardEventFilter = null | undefined | string | IKeyboardEventPredicate;
export type IKeyboardEventHandler = (event: KeyboardEvent) => void;
export type IKeyboardEventFilter = null | undefined | string | boolean | IKeyboardEventPredicate;
export type IKeyboardEventHandler<T extends EventTarget> = (this: T, event: KeyboardEvent) => void;

export type IUseKeyboardEventOptions<T extends EventTarget> = {
/**
* Event name that triggers handler.
* @default `keydown`
*/
event?: 'keydown' | 'keypress' | 'keyup';
/**
* Target that should emit event.
* @default window
*/
target?: RefObject<T> | T | null;
/**
* Options that will be passed to underlying `useEventListener` hook.
*/
eventOptions?: boolean | AddEventListenerOptions;
};

const createKeyPredicate = (keyFilter: IKeyboardEventFilter): IKeyboardEventPredicate => {
if (typeof keyFilter === 'function') return keyFilter;
if (typeof keyFilter === 'string') return (event: KeyboardEvent) => event.key === keyFilter;
if (typeof keyFilter === 'string') return (ev) => ev.key === keyFilter;
return keyFilter ? yieldTrue : yieldFalse;
};

const WINDOW_OR_NULL = isBrowser ? window : null;

/**
* React hook to execute a handler when a key is used
* it on unmount.
* Executes callback when keyboard event occurred on target (window by default).
*
* @param keyOrPredicate Key filter can be `string` or callback function accept a KeyboardEvent and return a boolean.
* @param callback callback function to call when key is used accept a KeyboardEvent
* @param deps Dependencies list that will be passed to underlying `useMemo`
* @param options some options passed to addEventListener, event can be `[keydown, keyup, keypress]`, target is the event target default `window`, eventOptions is the third parameter of `addEventListener`.
* @param keyOrPredicate Filters keypresses on which `callback` will be executed.
* @param callback Function to call when key is pressed and `keyOrPredicate` matches positive.
* @param deps Dependencies list that will be passed to underlying `useMemo`.
* @param options Hook options.
*/
export function useKeyboardEvent<T extends EventTarget>(
keyOrPredicate: IKeyboardEventFilter,
callback: IKeyboardEventHandler = noop,
deps: DependencyList = [keyOrPredicate],
callback: IKeyboardEventHandler<T>,
deps?: DependencyList,
options: IUseKeyboardEventOptions<T> = {}
): void {
const windowOrNull = isBrowser ? window : null;
const { event = 'keydown', target = windowOrNull, eventOptions } = options;
const memoHandler = useMemo(() => {
const predicate: IKeyboardEventPredicate = createKeyPredicate(keyOrPredicate);
// eslint-disable-next-line func-names
const handler: IKeyboardEventHandler = function (handlerEvent) {
if (predicate(handlerEvent)) {
// @ts-expect-error useEventListener will handle this reference to target
callback.call(this, handlerEvent);
const { event = 'keydown', target = WINDOW_OR_NULL, eventOptions } = options;
const cbRef = useSyncedRef(callback);

const handler = useMemo<IKeyboardEventHandler<T>>(() => {
const predicate = createKeyPredicate(keyOrPredicate);

return function kbEventHandler(this: T, ev) {
if (predicate(ev)) {
cbRef.current.call(this, ev);
}
};
return handler;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
useEventListener(target, event, memoHandler, eventOptions);

useEventListener(target, event, handler, eventOptions);
}

0 comments on commit a0e1b24

Please sign in to comment.