diff --git a/lib/keys.ts b/lib/keys.ts new file mode 100644 index 0000000..c3b335f --- /dev/null +++ b/lib/keys.ts @@ -0,0 +1,67 @@ +import { Source } from "./source"; + +export type KeyName = "up" | "down" | "left" | "right" | "a" | "b" | "menu"; + +const KEY_NAMES: Record<string, KeyName> = { + // compact keys (WASD+ZXC) + KeyZ: "a", + KeyX: "b", + KeyC: "menu", + KeyW: "up", + KeyS: "down", + KeyA: "left", + KeyD: "right", + // full-board keys (arrows+space/shift/enter) + Space: "a", + ShiftLeft: "b", + ShiftRight: "b", + Enter: "menu", + ArrowUp: "up", + ArrowDown: "down", + ArrowLeft: "left", + ArrowRight: "right", +}; + +/** A keypress/release event for an abstract button, or else ["focus", "release"] if for some reason future release events might not be registered. */ +export type KeyEvent = [KeyName | "focus", "press" | "release"]; + +export function keyControl(source: HTMLElement): Source<KeyEvent> { + const tabIndex = source.getAttribute("tabIndex"); + source.setAttribute( + "tabindex", + tabIndex == "" || tabIndex == null ? "-1" : tabIndex + ); + + return ((callback?: (keyEvent: KeyEvent) => void) => { + if (callback) { + const handle = (evt: KeyboardEvent, action: "press" | "release") => { + const keyName = KEY_NAMES[evt.code]; + if (keyName != undefined) { + evt.preventDefault(); + evt.stopPropagation(); + + callback([keyName, action]); + } + }; + const keyUp = (evt: KeyboardEvent) => handle(evt, "release"); + const keyDown = (evt: KeyboardEvent) => handle(evt, "press"); + const focus = () => callback(["focus", "press"]); + const blur = () => callback(["focus", "release"]); + + source.addEventListener("keyup", keyUp, false); + source.addEventListener("keydown", keyDown, false); + source.addEventListener("focus", focus, false); + source.addEventListener("blur", blur, false); + source.focus({ focusVisible: true } as FocusOptions); + + return () => { + source.removeEventListener("keyup", keyUp, false); + source.removeEventListener("keydown", keyDown, false); + source.removeEventListener("focus", focus, false); + source.removeEventListener("blur", blur, false); + }; + } else { + return ["blur", "release"]; + } + }) as Source<KeyEvent>; +}