82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
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]);
|
|
}
|