Skip to content

Commit

Permalink
feat: useKeyboard hooks (#541)
Browse files Browse the repository at this point in the history
* feat(keyboard): create keyboard hooks

* feat(usekeyboard): redesign event handler to match keyboard events from browser

\

* test(usekeyboard): add testcase

* docs(usekeyboard): create new hooks document
  • Loading branch information
unix committed Aug 13, 2021
1 parent d6fcafa commit a3fb056
Show file tree
Hide file tree
Showing 12 changed files with 570 additions and 8 deletions.
1 change: 1 addition & 0 deletions components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { default as useClickAway } from './use-click-away'
export { default as useClipboard } from './use-clipboard'
export { default as useCurrentState } from './use-current-state'
export { default as useMediaQuery } from './use-media-query'
export { default as useKeyboard, KeyMod, KeyCode } from './use-keyboard'
export { default as Avatar } from './avatar'
export { default as Text } from './text'
export { default as Note } from './note'
Expand Down
177 changes: 177 additions & 0 deletions components/use-keyboard/__tests__/keyboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import React from 'react'
import { mount } from 'enzyme'
import { useKeyboard, KeyMod, KeyCode } from 'components'
import { renderHook, act } from '@testing-library/react-hooks'
import { KeyboardResult } from '../use-keyboard'

describe('UseKeyboard', () => {
it('should work correctly', () => {
let code = null
const handler = jest.fn().mockImplementation(e => {
code = e.keyCode
})
renderHook(() => useKeyboard(handler, KeyCode.KEY_H))
document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: KeyCode.KEY_H }))
expect(handler).toBeCalledTimes(1)
expect(code).toEqual(KeyCode.KEY_H)
})

it('should not trigger handler', () => {
const handler = jest.fn().mockImplementation(() => {})
renderHook(() => useKeyboard(handler, [KeyCode.KEY_0]))
const event = new KeyboardEvent('keydown', { keyCode: KeyCode.KEY_1 })
document.dispatchEvent(event)
expect(handler).not.toBeCalled()
})

it('should trigger with command key', () => {
const handler = jest.fn().mockImplementation(() => {})
renderHook(() => useKeyboard(handler, [KeyCode.KEY_A, KeyMod.Shift]))
const event = new KeyboardEvent('keydown', { keyCode: KeyCode.KEY_A })
document.dispatchEvent(event)
expect(handler).not.toBeCalled()
const event2 = new KeyboardEvent('keydown', {
keyCode: KeyCode.KEY_A,
shiftKey: true,
})
document.dispatchEvent(event2)
expect(handler).toBeCalledTimes(1)
})

it('should ignore command when code does not exist', () => {
const handler = jest.fn().mockImplementation(() => {})
renderHook(() => useKeyboard(handler, [KeyCode.KEY_A, 12345]))
const event = new KeyboardEvent('keydown', { keyCode: KeyCode.KEY_A })
document.dispatchEvent(event)
expect(handler).toBeCalled()
})

it('should work with each command', () => {
const handler = jest.fn().mockImplementation(() => {})
renderHook(() =>
useKeyboard(handler, [KeyCode.KEY_A, KeyMod.Alt, KeyMod.CtrlCmd, KeyMod.WinCtrl]),
)
document.dispatchEvent(
new KeyboardEvent('keydown', {
keyCode: KeyCode.KEY_A,
}),
)
expect(handler).not.toBeCalled()
document.dispatchEvent(
new KeyboardEvent('keydown', {
keyCode: KeyCode.KEY_A,
altKey: true,
}),
)
expect(handler).not.toBeCalled()
document.dispatchEvent(
new KeyboardEvent('keydown', {
keyCode: KeyCode.KEY_A,
altKey: true,
ctrlKey: true,
}),
)
expect(handler).not.toBeCalled()
document.dispatchEvent(
new KeyboardEvent('keydown', {
keyCode: KeyCode.KEY_A,
altKey: true,
ctrlKey: true,
metaKey: true,
}),
)
expect(handler).toBeCalledTimes(1)
})

it('should ignore global events', () => {
const handler = jest.fn().mockImplementation(() => {})
renderHook(() => useKeyboard(handler, [KeyCode.KEY_A], { disableGlobalEvent: true }))
const event = new KeyboardEvent('keydown', { keyCode: KeyCode.KEY_A })
document.dispatchEvent(event)
expect(handler).not.toBeCalled()
})

it('should respond to different event types', () => {
const handler = jest.fn().mockImplementation(() => {})
renderHook(() => useKeyboard(handler, [KeyCode.KEY_A], { event: 'keyup' }))
document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: KeyCode.KEY_A }))
expect(handler).not.toBeCalled()

document.dispatchEvent(new KeyboardEvent('keypress', { keyCode: KeyCode.KEY_A }))
expect(handler).not.toBeCalled()

document.dispatchEvent(new KeyboardEvent('keyup', { keyCode: KeyCode.KEY_A }))
expect(handler).toBeCalled()
})

it('should pass the keyboard events', () => {
const handler = jest.fn().mockImplementation(() => {})
const nativeHandler = jest.fn().mockImplementation(() => {})
const { result } = renderHook<void, KeyboardResult>(() =>
useKeyboard(handler, KeyCode.Escape),
)
const wrapper = mount(
<div onKeyDown={nativeHandler}>
<span id="inner" {...result.current.bindings} />
</div>,
)
const inner = wrapper.find('#inner').at(0)
act(() => {
inner.simulate('keyup', {
keyCode: KeyCode.Escape,
})
})
expect(handler).not.toBeCalled()
expect(nativeHandler).not.toBeCalled()
act(() => {
inner.simulate('keydown', {
keyCode: KeyCode.Escape,
})
})
expect(handler).toBeCalled()
expect(nativeHandler).toBeCalled()
})

it('should prevent default events', () => {
const handler = jest.fn().mockImplementation(() => {})
const nativeHandler = jest.fn().mockImplementation(() => {})
const { result } = renderHook<void, KeyboardResult>(() =>
useKeyboard(handler, KeyCode.Escape, {
disableGlobalEvent: true,
stopPropagation: true,
}),
)
const wrapper = mount(
<div onKeyDown={nativeHandler}>
<span id="inner" {...result.current.bindings} />
</div>,
)
const inner = wrapper.find('#inner').at(0)
act(() => {
inner.simulate('keydown', {
keyCode: KeyCode.Escape,
})
})
expect(handler).toBeCalled()
expect(nativeHandler).not.toBeCalled()
})

it('should trigger capture event', () => {
const handler = jest.fn().mockImplementation(() => {})
const { result } = renderHook<void, KeyboardResult>(() =>
useKeyboard(handler, KeyCode.Escape, { capture: true, disableGlobalEvent: true }),
)
const wrapper = mount(
<div onKeyDownCapture={result.current.bindings.onKeyDownCapture}>
<span id="inner" />
</div>,
)
const inner = wrapper.find('#inner').at(0)
act(() => {
inner.simulate('keydown', {
keyCode: KeyCode.Escape,
})
})
expect(handler).toBeCalled()
})
})
94 changes: 94 additions & 0 deletions components/use-keyboard/codes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* KeyBinding Codes
* The content of this file is based on the design of the open source project "microsoft/vscode",
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* We inherit the KeyMod values from "microsoft/vscode",
* but use the Browser's KeyboardEvent event implementation, and all values are used only as identification.
*/

export enum KeyCode {
Unknown = 0,
Backspace = 8,
Tab = 9,
Enter = 13,
Shift = 16,
Ctrl = 17,
Alt = 18,
PauseBreak = 19,
CapsLock = 20,
Escape = 27,
Space = 32,
PageUp = 33,
PageDown = 34,
End = 35,
Home = 36,
LeftArrow = 37,
UpArrow = 38,
RightArrow = 39,
DownArrow = 40,
Insert = 45,
Delete = 46,
KEY_0 = 48,
KEY_1 = 49,
KEY_2 = 50,
KEY_3 = 51,
KEY_4 = 52,
KEY_5 = 53,
KEY_6 = 54,
KEY_7 = 55,
KEY_8 = 56,
KEY_9 = 57,
KEY_A = 65,
KEY_B = 66,
KEY_C = 67,
KEY_D = 68,
KEY_E = 69,
KEY_F = 70,
KEY_G = 71,
KEY_H = 72,
KEY_I = 73,
KEY_J = 74,
KEY_K = 75,
KEY_L = 76,
KEY_M = 77,
KEY_N = 78,
KEY_O = 79,
KEY_P = 80,
KEY_Q = 81,
KEY_R = 82,
KEY_S = 83,
KEY_T = 84,
KEY_U = 85,
KEY_V = 86,
KEY_W = 87,
KEY_X = 88,
KEY_Y = 89,
KEY_Z = 90,
Meta = 91,
F1 = 112,
F2 = 113,
F3 = 114,
F4 = 115,
F5 = 116,
F6 = 117,
F7 = 118,
F8 = 119,
F9 = 120,
F10 = 121,
F11 = 122,
F12 = 123,
NumLock = 144,
ScrollLock = 145,
Equal = 187,
Minus = 189,
Backquote = 192,
Backslash = 220,
}

export enum KeyMod {
CtrlCmd = (1 << 11) >>> 0,
Shift = (1 << 10) >>> 0,
Alt = (1 << 9) >>> 0,
WinCtrl = (1 << 8) >>> 0,
}
27 changes: 27 additions & 0 deletions components/use-keyboard/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { isMac } from '../utils/collections'
import { KeyMod } from './codes'

/* istanbul ignore next */
export const getCtrlKeysByPlatform = (): Record<string, 'metaKey' | 'ctrlKey'> => {
return {
CtrlCmd: isMac() ? 'metaKey' : 'ctrlKey',
WinCtrl: isMac() ? 'ctrlKey' : 'metaKey',
}
}

export const getActiveModMap = (
bindings: number[],
): Record<keyof typeof KeyMod, boolean> => {
const modBindings = bindings.filter((item: number) => !!KeyMod[item])
const activeModMap: Record<keyof typeof KeyMod, boolean> = {
CtrlCmd: false,
Shift: false,
Alt: false,
WinCtrl: false,
}
modBindings.forEach(code => {
const modKey = KeyMod[code] as keyof typeof KeyMod
activeModMap[modKey] = true
})
return activeModMap
}
5 changes: 5 additions & 0 deletions components/use-keyboard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import useKeyboard from './use-keyboard'
import { KeyMod, KeyCode } from './codes'

export default useKeyboard
export { KeyMod, KeyCode }
Loading

0 comments on commit a3fb056

Please sign in to comment.