mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
refactor(replay-ui): extract keyboard-nav dispatch into pure module
This commit is contained in:
1 parent
e03da83cca
commit
153f17c756
2 files changed
+91
-64
No files matched your search
@@ -1,73 +1,24 @@
|
||||
import { useEffect } from "react";
|
||||
import { dispatchKey, type KeyboardNavOptions } from "../lib/keyboard-nav";
|
||||
|
||||
export interface UseKeyboardNavOptions {
|
||||
onPrev: () => void;
|
||||
onNext: () => void;
|
||||
onJumpStart: () => void;
|
||||
onJumpEnd: () => void;
|
||||
onJumpPrev10: () => void;
|
||||
onJumpNext10: () => void;
|
||||
onJumpNextViolation: () => void;
|
||||
}
|
||||
|
||||
const NAVIGATION_TAGS = new Set(["INPUT", "TEXTAREA", "SELECT"]);
|
||||
const ARROW_OWNING_ROLES = new Set(["tab", "tablist", "option", "listbox", "menuitem", "menu"]);
|
||||
|
||||
function targetOwnsArrowKeys(target: HTMLElement | null): boolean {
|
||||
let node: HTMLElement | null = target;
|
||||
while (node) {
|
||||
const role = node.getAttribute?.("role");
|
||||
if (role && ARROW_OWNING_ROLES.has(role)) return true;
|
||||
node = node.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export type UseKeyboardNavOptions = KeyboardNavOptions;
|
||||
|
||||
export function useKeyboardNav(options: UseKeyboardNavOptions) {
|
||||
useEffect(() => {
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (target && (NAVIGATION_TAGS.has(target.tagName) || target.isContentEditable)) {
|
||||
return;
|
||||
}
|
||||
const isArrow = event.key.startsWith("Arrow");
|
||||
if (isArrow && targetOwnsArrowKeys(target)) {
|
||||
return;
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey || event.altKey) {
|
||||
return;
|
||||
}
|
||||
switch (event.key) {
|
||||
case "ArrowLeft":
|
||||
case "k":
|
||||
if (event.shiftKey) {
|
||||
options.onJumpPrev10();
|
||||
} else {
|
||||
options.onPrev();
|
||||
}
|
||||
event.preventDefault();
|
||||
return;
|
||||
case "ArrowRight":
|
||||
case "j":
|
||||
if (event.shiftKey) {
|
||||
options.onJumpNext10();
|
||||
} else {
|
||||
options.onNext();
|
||||
}
|
||||
event.preventDefault();
|
||||
return;
|
||||
case "g":
|
||||
options.onJumpStart();
|
||||
event.preventDefault();
|
||||
return;
|
||||
case "G":
|
||||
options.onJumpEnd();
|
||||
event.preventDefault();
|
||||
return;
|
||||
case ".":
|
||||
options.onJumpNextViolation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
const handled = dispatchKey(
|
||||
{
|
||||
key: event.key,
|
||||
shiftKey: event.shiftKey,
|
||||
metaKey: event.metaKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
altKey: event.altKey,
|
||||
target: event.target as HTMLElement | null,
|
||||
},
|
||||
options,
|
||||
);
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
export interface KeyboardNavOptions {
|
||||
onPrev: () => void;
|
||||
onNext: () => void;
|
||||
onJumpStart: () => void;
|
||||
onJumpEnd: () => void;
|
||||
onJumpPrev10: () => void;
|
||||
onJumpNext10: () => void;
|
||||
onJumpNextViolation: () => void;
|
||||
}
|
||||
|
||||
export interface KeyEvent {
|
||||
key: string;
|
||||
shiftKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
altKey?: boolean;
|
||||
target?: HTMLElement | null;
|
||||
}
|
||||
|
||||
const NAVIGATION_TAGS = new Set(["INPUT", "TEXTAREA", "SELECT"]);
|
||||
const ARROW_OWNING_ROLES = new Set([
|
||||
"tab",
|
||||
"tablist",
|
||||
"option",
|
||||
"listbox",
|
||||
"menuitem",
|
||||
"menu",
|
||||
]);
|
||||
|
||||
export function targetOwnsArrowKeys(target: HTMLElement | null): boolean {
|
||||
let node: HTMLElement | null = target;
|
||||
while (node) {
|
||||
const role = node.getAttribute?.("role");
|
||||
if (role && ARROW_OWNING_ROLES.has(role)) return true;
|
||||
node = node.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns true when the key was consumed (caller should preventDefault).
|
||||
export function dispatchKey(
|
||||
event: KeyEvent,
|
||||
options: KeyboardNavOptions,
|
||||
): boolean {
|
||||
const target = event.target ?? null;
|
||||
if (target && (NAVIGATION_TAGS.has(target.tagName) || target.isContentEditable)) {
|
||||
return false;
|
||||
}
|
||||
if (event.key.startsWith("Arrow") && targetOwnsArrowKeys(target)) {
|
||||
return false;
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey || event.altKey) {
|
||||
return false;
|
||||
}
|
||||
switch (event.key) {
|
||||
case "ArrowLeft":
|
||||
case "k":
|
||||
(event.shiftKey ? options.onJumpPrev10 : options.onPrev)();
|
||||
return true;
|
||||
case "ArrowRight":
|
||||
case "j":
|
||||
(event.shiftKey ? options.onJumpNext10 : options.onNext)();
|
||||
return true;
|
||||
case "g":
|
||||
options.onJumpStart();
|
||||
return true;
|
||||
case "G":
|
||||
options.onJumpEnd();
|
||||
return true;
|
||||
case ".":
|
||||
options.onJumpNextViolation();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user