feat(frontend): useDrawerKeyboard hook for j/k/esc/?
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
/** Tag names whose descendants can receive free-form text input. */
|
||||
const INPUT_TAGS = new Set(["INPUT", "TEXTAREA", "SELECT"]);
|
||||
|
||||
/**
|
||||
* Returns true when the keyboard event originated inside a free-form
|
||||
* text field, in which case we should NOT consume j/k/escape/`?` —
|
||||
* the user is typing, not navigating the drawer.
|
||||
*
|
||||
* - `INPUT` / `TEXTAREA` / `SELECT` are the obvious cases.
|
||||
* - `contenteditable` elements (e.g. rich text editors) are the
|
||||
* non-obvious one: `target.isContentEditable` is the standard check.
|
||||
*/
|
||||
function isEditableTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
if (INPUT_TAGS.has(target.tagName)) return true;
|
||||
return target.isContentEditable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires the spec-driven drawer keyboard shortcuts to the drawer's
|
||||
* callbacks. When `enabled` is true, listens for `keydown` on `window`
|
||||
* and dispatches:
|
||||
*
|
||||
* j / ArrowDown → onNext
|
||||
* k / ArrowUp → onPrev
|
||||
* Escape → onClose
|
||||
* ? → onToggleHelp
|
||||
*
|
||||
* Keys are ignored when an `<input>`, `<textarea>`, `<select>`, or
|
||||
* `contenteditable` element has focus — typing "j" into a search box
|
||||
* must not navigate claims. `preventDefault` is called for each matched
|
||||
* key to suppress the browser's default (arrow keys scrolling the page,
|
||||
* Escape closing modals/pausing video, etc.).
|
||||
*
|
||||
* The effect tears down on unmount or when `enabled` flips to false, and
|
||||
* re-subscribes whenever any callback identity changes (so consumers
|
||||
* don't have to memoize their handlers to keep the listener stable).
|
||||
*/
|
||||
export function useDrawerKeyboard(opts: {
|
||||
enabled: boolean;
|
||||
onNext: () => void;
|
||||
onPrev: () => void;
|
||||
onClose: () => void;
|
||||
onToggleHelp: () => void;
|
||||
}): void {
|
||||
const { enabled, onNext, onPrev, onClose, onToggleHelp } = opts;
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (isEditableTarget(e.target)) return;
|
||||
|
||||
switch (e.key) {
|
||||
case "j":
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
onNext();
|
||||
break;
|
||||
case "k":
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
onPrev();
|
||||
break;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
break;
|
||||
case "?":
|
||||
e.preventDefault();
|
||||
onToggleHelp();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [enabled, onNext, onPrev, onClose, onToggleHelp]);
|
||||
}
|
||||
Reference in New Issue
Block a user