Skip to content

Commit 1b5373c

Browse files
committed
useKeyDownHandler: migrate to typescript and add unit test
1 parent e67e015 commit 1b5373c

File tree

3 files changed

+93
-28
lines changed

3 files changed

+93
-28
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
"tsx": "never"
3030
}
3131
],
32-
"react/jsx-filename-extension": [1, { "extensions": [".jsx", ".tsx"] }],
3332
"import/prefer-default-export": "off",
33+
"react/jsx-filename-extension": [1, { "extensions": [".jsx", ".tsx"] }],
3434
"comma-dangle": 0, // not sure why airbnb turned this on. gross!
3535
"default-param-last": 0,
3636
"no-else-return" :0,
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import useKeyDownHandlers from './useKeyDownHandlers';
4+
import { isMac } from '../utils/device';
5+
6+
jest.mock('../utils/device');
7+
8+
function fireKeyboardEvent(key: string, options: Partial<KeyboardEvent> = {}) {
9+
const event = new KeyboardEvent('keydown', {
10+
key,
11+
code: `Key${key.toUpperCase()}`,
12+
bubbles: true,
13+
cancelable: true,
14+
...options
15+
});
16+
document.dispatchEvent(event);
17+
}
18+
19+
// Component for testing the hook
20+
const HookConsumer = ({
21+
handlers
22+
}: {
23+
handlers: Record<string, jest.Mock>;
24+
}) => {
25+
useKeyDownHandlers(handlers);
26+
return null;
27+
};
28+
29+
describe('useKeyDownHandlers', () => {
30+
let handlers: Record<string, jest.Mock>;
31+
32+
beforeEach(() => {
33+
handlers = {
34+
f: jest.fn(),
35+
'ctrl-f': jest.fn(),
36+
'ctrl-shift-f': jest.fn(),
37+
'ctrl-alt-n': jest.fn()
38+
};
39+
});
40+
41+
afterEach(() => {
42+
jest.clearAllMocks();
43+
});
44+
45+
it('calls "ctrl-f" handler on Windows (isMac false)', () => {
46+
(isMac as jest.Mock).mockReturnValue(false);
47+
48+
render(<HookConsumer handlers={handlers} />);
49+
fireKeyboardEvent('f', { ctrlKey: true });
50+
51+
expect(handlers['ctrl-f']).toHaveBeenCalled();
52+
expect(handlers.f).toHaveBeenCalled();
53+
});
54+
55+
it('calls "ctrl-f" handler on Mac (isMac true)', () => {
56+
(isMac as jest.Mock).mockReturnValue(true);
57+
58+
render(<HookConsumer handlers={handlers} />);
59+
fireKeyboardEvent('f', { metaKey: true });
60+
61+
expect(handlers['ctrl-f']).toHaveBeenCalled();
62+
expect(handlers.f).toHaveBeenCalled();
63+
});
64+
65+
it('calls "ctrl-shift-f" handler with both ctrl and shift keys', () => {
66+
(isMac as jest.Mock).mockReturnValue(false);
67+
68+
render(<HookConsumer handlers={handlers} />);
69+
fireKeyboardEvent('f', { ctrlKey: true, shiftKey: true });
70+
71+
expect(handlers['ctrl-shift-f']).toHaveBeenCalled();
72+
});
73+
74+
it('calls "ctrl-alt-n" handler with ctrl and alt', () => {
75+
(isMac as jest.Mock).mockReturnValue(false);
76+
77+
render(<HookConsumer handlers={handlers} />);
78+
fireKeyboardEvent('n', { ctrlKey: true, altKey: true, code: 'KeyN' });
79+
80+
expect(handlers['ctrl-alt-n']).toHaveBeenCalled();
81+
});
82+
});

client/common/useKeyDownHandlers.ts

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { mapKeys } from 'lodash';
2-
import PropTypes from 'prop-types';
32
import { useCallback, useEffect, useRef } from 'react';
3+
import { isMac } from '../utils/device';
4+
5+
/** Function to call upon keydown */
6+
export type KeydownHandler = (e: KeyboardEvent) => void;
7+
/** An object mapping from keys like 'ctrl-s' or 'ctrl-shift-1' to handlers. */
8+
export type KeydownHandlerMap = Record<string, KeydownHandler>;
49

510
/**
611
* Attaches keydown handlers to the global document.
7-
*
812
* Handles Mac/PC switching of Ctrl to Cmd.
9-
*
10-
* @param {Record<string, (e: KeyboardEvent) => void>} keyHandlers - an object
11-
* which maps from the key to its event handler. The object keys are a combination
12-
* of the key and prefixes `ctrl-` `shift-` (ie. 'ctrl-f', 'ctrl-shift-f')
13-
* and the values are the function to call when that specific key is pressed.
13+
* @param keyHandlers - an object which maps from the key to its event handler. The object keys are a combination of the key and prefixes `ctrl-` `shift-`
14+
* (ie. 'ctrl-f', 'ctrl-shift-f') and the values are the function to call when that specific key is pressed.
1415
*/
15-
export default function useKeyDownHandlers(keyHandlers) {
16+
export default function useKeyDownHandlers(keyHandlers: KeydownHandlerMap) {
1617
/**
1718
* Instead of memoizing the handlers, use a ref and call the current
1819
* handler at the time of the event.
@@ -30,8 +31,7 @@ export default function useKeyDownHandlers(keyHandlers) {
3031
*/
3132
const handleEvent = useCallback((e) => {
3233
if (!e.key) return;
33-
const isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
34-
const isCtrl = isMac ? e.metaKey : e.ctrlKey;
34+
const isCtrl = isMac() ? e.metaKey : e.ctrlKey;
3535
if (e.shiftKey && isCtrl) {
3636
handlers.current[
3737
`ctrl-shift-${
@@ -53,20 +53,3 @@ export default function useKeyDownHandlers(keyHandlers) {
5353
return () => document.removeEventListener('keydown', handleEvent);
5454
}, [handleEvent]);
5555
}
56-
57-
/**
58-
* Component version can be used in class components where hooks can't be used.
59-
*
60-
* @param {Record<string, (e: KeyboardEvent) => void>} handlers
61-
*/
62-
export const DocumentKeyDown = ({ handlers }) => {
63-
useKeyDownHandlers(handlers);
64-
return null;
65-
};
66-
DocumentKeyDown.propTypes = {
67-
handlers: PropTypes.objectOf(PropTypes.func)
68-
};
69-
70-
DocumentKeyDown.defaultProps = {
71-
handlers: {}
72-
};

0 commit comments

Comments
 (0)