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 { useEffect } from "react";
|
||||||
|
import { dispatchKey, type KeyboardNavOptions } from "../lib/keyboard-nav";
|
||||||
|
|
||||||
export interface UseKeyboardNavOptions {
|
export type UseKeyboardNavOptions = KeyboardNavOptions;
|
||||||
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 function useKeyboardNav(options: UseKeyboardNavOptions) {
|
export function useKeyboardNav(options: UseKeyboardNavOptions) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (event: KeyboardEvent) => {
|
const handler = (event: KeyboardEvent) => {
|
||||||
const target = event.target as HTMLElement | null;
|
const handled = dispatchKey(
|
||||||
if (target && (NAVIGATION_TAGS.has(target.tagName) || target.isContentEditable)) {
|
{
|
||||||
return;
|
key: event.key,
|
||||||
}
|
shiftKey: event.shiftKey,
|
||||||
const isArrow = event.key.startsWith("Arrow");
|
metaKey: event.metaKey,
|
||||||
if (isArrow && targetOwnsArrowKeys(target)) {
|
ctrlKey: event.ctrlKey,
|
||||||
return;
|
altKey: event.altKey,
|
||||||
}
|
target: event.target as HTMLElement | null,
|
||||||
if (event.metaKey || event.ctrlKey || event.altKey) {
|
},
|
||||||
return;
|
options,
|
||||||
}
|
);
|
||||||
switch (event.key) {
|
if (handled) {
|
||||||
case "ArrowLeft":
|
event.preventDefault();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener("keydown", handler);
|
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