From be4673f138bf63cddf1121aa7863a8bfe6fa16e0 Mon Sep 17 00:00:00 2001 From: Remington Breeze Date: Tue, 10 Aug 2021 11:08:00 -0700 Subject: [PATCH] fix: Insource react-keyhooks package (#120) Signed-off-by: Remington Breeze --- v2/components/action-button/action-button.tsx | 2 +- v2/components/autocomplete/autocomplete.tsx | 2 +- v2/package.json | 3 +- v2/shared/index.ts | 1 + v2/shared/keypress.tsx | 248 ++++++++++++++++++ 5 files changed, 252 insertions(+), 4 deletions(-) create mode 100644 v2/shared/keypress.tsx diff --git a/v2/components/action-button/action-button.tsx b/v2/components/action-button/action-button.tsx index f9778bafaa..a4b6b96bf4 100644 --- a/v2/components/action-button/action-button.tsx +++ b/v2/components/action-button/action-button.tsx @@ -1,4 +1,4 @@ -import {Key, useKeyListener} from 'react-keyhooks'; +import {Key, useKeyListener} from '../../shared'; import * as React from 'react'; import {useClickOutside, useTimeout} from '../../utils/utils'; import {EffectDiv} from '../effect-div/effect-div'; diff --git a/v2/components/autocomplete/autocomplete.tsx b/v2/components/autocomplete/autocomplete.tsx index e3c54cae2d..c6510c41ff 100644 --- a/v2/components/autocomplete/autocomplete.tsx +++ b/v2/components/autocomplete/autocomplete.tsx @@ -1,4 +1,4 @@ -import {Key, KeybindingContext, KeybindingProvider, useNav} from 'react-keyhooks'; +import {Key, KeybindingContext, KeybindingProvider, useNav} from '../../shared'; import * as React from 'react'; import {Input, InputProps, SetInputFxn, useDebounce, useInput} from '../input/input'; import ThemeDiv from '../theme-div/theme-div'; diff --git a/v2/package.json b/v2/package.json index 12f9968119..ed8e7a83fd 100644 --- a/v2/package.json +++ b/v2/package.json @@ -26,8 +26,7 @@ "moment": "^2.29.1", "react": "^16.9.3", "react-dom": "^16.9.3", - "rxjs": "^6.6.6", - "react-keyhooks": "^0.2.2" + "rxjs": "^6.6.6" }, "scripts": { "start": "start-storybook", diff --git a/v2/shared/index.ts b/v2/shared/index.ts index 02dd2f7d57..ec64a3b869 100644 --- a/v2/shared/index.ts +++ b/v2/shared/index.ts @@ -1 +1,2 @@ export * from './context/theme'; +export * from './keypress'; \ No newline at end of file diff --git a/v2/shared/keypress.tsx b/v2/shared/keypress.tsx new file mode 100644 index 0000000000..c9be019785 --- /dev/null +++ b/v2/shared/keypress.tsx @@ -0,0 +1,248 @@ +import * as React from 'react'; + +export enum Key { + TAB = 9, + ENTER = 13, + SHIFT = 16, + ESCAPE = 27, + LEFT = 37, + UP = 38, + RIGHT = 39, + DOWN = 40, + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + SLASH = 191, + QUESTION = 191, +} + +export enum NumKey { + ZERO = 48, + ONE = 49, + TWO = 50, + THREE = 51, + FOUR = 52, + FIVE = 53, + SIX = 54, + SEVEN = 55, + EIGHT = 56, + NINE = 57, +} + +export enum NumPadKey { + ZERO = 96, + ONE = 97, + TWO = 98, + THREE = 99, + FOUR = 100, + FIVE = 101, + SIX = 102, + SEVEN = 103, + EIGHT = 104, + NINE = 105, +} + +export type AnyNumKey = NumKey | NumPadKey; +export type AnyKeys = AnyNumKey | Key | (AnyNumKey | Key)[]; + +// useNav adds simple stateful navigation to your component +// Returns: +// - pos: indicates current position +// - nav: fxn that accepts an integer that represents number to increment/decrement pos +// - reset: fxn that sets current position to -1 +// Accepts: +// - upperBound: maximum value that pos can grow to +// - init: optional initial value for pos + +export const useNav = (upperBound: number, init?: number): [number, (n: number) => boolean, () => void] => { + const [pos, setPos] = React.useState(init || -1); + const isInBounds = (p: number): boolean => p < upperBound && p > -1; + + const nav = (val: number): boolean => { + const newPos = pos + val; + return isInBounds(newPos) ? setPos(newPos) === null : false; + }; + + const reset = () => { + setPos(-1); + }; + + return [pos, nav, reset]; +}; + +export type KeyState = {action: KeyAction; pressed: boolean; group: number}; +export type KeyAction = (keyCode?: number) => boolean; +export type KeyMap = {[key: number]: KeyState}; +export type KeyHandler = (e: KeyboardEvent) => null; + +export type KeyFxn = (keys: AnyKeys, action: KeyAction, combo?: boolean) => void; + +export interface GroupMap { + groupForKey: {[key: number]: number}; + groups: {[group: number]: KeyMap}; + index: number; +} + +const handlePress = (e: KeyboardEvent, state: GroupMap) => { + const {groups, groupForKey} = state; + const g = groupForKey[e.keyCode]; + if (groups[g]) { + let allPressed = true; + groups[g][e.keyCode].pressed = true; + + for (const i of Object.keys(groups[g])) { + const k = parseInt(i, 10); + const key = groups[g][k]; + + if (!key.pressed) { + allPressed = false; + } + } + + if (allPressed) { + const prevent = groups[g][e.keyCode].action(e.keyCode); + if (prevent) { + e.preventDefault(); + } + } + } +}; + +const handleKeyUp = (e: KeyboardEvent, state: GroupMap) => { + const {groups, groupForKey} = state; + const g = groupForKey[e.keyCode]; + if (groups[g]) { + groups[g][e.keyCode].pressed = false; + } +}; + +const useKeyListen = (state: GroupMap) => { + const localKeyPress = (e: KeyboardEvent) => handlePress(e, state); + const localKeyUp = (e: KeyboardEvent) => handleKeyUp(e, state); + + React.useEffect(() => { + document.addEventListener('keydown', localKeyPress); + document.addEventListener('keyup', localKeyUp); + return () => { + document.removeEventListener('keydown', localKeyPress); + document.removeEventListener('keyup', localKeyUp); + }; + }, [state]); +}; + +export const useKeyListener = () => { + let state = NewGroupMap(); + useKeyListen(state); + return (keys: AnyKeys, action: KeyAction, combo?: boolean) => { + state = addKeybinding(state, keys, action, combo); + }; +}; + +export const useSharedKeyListener = (): GroupMap => { + const state = NewGroupMap(); + useKeyListen(state); + return state; +}; + +const NewGroupMap = () => { + const groupForKey = {} as {[key: number]: number}; + const groups = {} as {[group: number]: KeyMap}; + return { + groups, + groupForKey, + index: 0, + }; +}; + +export const addKeybinding = (state: GroupMap, keys: AnyKeys, action: KeyAction, combo?: boolean): GroupMap => { + const {groups, groupForKey} = state; + let index = state.index; + if (Array.isArray(keys)) { + let g = index; + for (const key of keys) { + // create association between this key and its group + groupForKey[key] = index; + + if (!groups[g]) { + groups[index] = {} as KeyMap; + } + + groups[index][key] = { + group: g, + action, + pressed: false, + }; + + if (!combo) { + g = g + 1; + } + } + index = g + 1; + } else { + groupForKey[keys] = index; + + if (!groups[index]) { + groups[index] = {} as KeyMap; + } + + groups[index][keys] = { + group: index, + action, + pressed: false, + }; + + index = index + 1; + } + + return {groups, groupForKey, index}; +}; + +export const NumKeyToNumber = (key: AnyNumKey): number => { + if (key > 47 && key < 58) { + return key - 48; + } else if (key > 95 && key < 106) { + return key - 96; + } + return -1; +}; + +export const KeybindingContext = React.createContext<{ + keybindingState: GroupMap; + useKeybinding: KeyFxn; +}>({ + keybindingState: NewGroupMap(), + useKeybinding: (keys: AnyKeys, action: KeyAction, combo?: boolean) => null, +}); + +export const KeybindingProvider = (props: {children: React.ReactNode}) => { + let keybindingState: GroupMap = useSharedKeyListener(); + + const useKeybinding = (keys: AnyKeys, action: KeyAction, combo?: boolean) => { + keybindingState = addKeybinding(keybindingState, keys, action, combo); + }; + + return {props.children}; +};