app/components/admin/media-keyboard.tsx
72 lines and 6 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| tileLink | 6 to 10 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| useGridKeyboard | 19 to 79 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:28> | 28 to 31 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:36> | 36 to 74 | 15 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| move | 42 to 54 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:45> | 45 to 48 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
Imports
It imports app/lib/media/tile-nav.mjs. Imported by app/components/admin/media-grid.tsx.
Source
import { useCallback } from "react";
import { bands, nextTile } from "~/lib/media/tile-nav.mjs";
/** The tile's tab stop in the grid: the thumbnail link, which carries the file's name. */
function tileLink(key: string) {
return document.querySelector<HTMLElement>(
`.cap-media-grid [data-tile="${CSS.escape(key)}"] a.cap-media-open`,
);
}
/**
* The grid's keys, on the grid alone: they act only while focus is on a tile, so a key pressed on a
* button, a link or the inspector belongs to that control (WCAG 2.1.4). Focus is real and roves:
* one tile's controls are in the tab order, the arrows move focus to another tile, and the page
* renders that tile as the tab stop. Enter is the link's own. Rows come from rendered geometry, not a
* column model: `bands()` in tile-nav.mjs says why.
*/
export function useGridKeyboard({
setActive,
setSelected,
}: {
setActive: (key: string) => void;
setSelected: (keys: string[]) => void;
}) {
// Focus arriving on a tile by any route (Tab, a click, the inspector closing) makes it the stop.
const onFocus = useCallback(
(event: React.FocusEvent<HTMLElement>) => {
const key = (event.target as HTMLElement).closest("[data-tile]")?.getAttribute("data-tile");
if (key) setActive(key);
},
[setActive],
);
const onKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLElement>) => {
if (event.metaKey || event.ctrlKey || event.altKey) return;
const tile = (event.target as HTMLElement).closest("[data-tile]");
const here = tile?.getAttribute("data-tile");
if (!tile || !here) return;
const move = (dir: "left" | "right" | "up" | "down") => {
event.preventDefault();
const rows = bands(
[...document.querySelectorAll(".cap-media-grid [data-tile]")].map((el) => {
const r = el.getBoundingClientRect();
return { id: el.getAttribute("data-tile") ?? "", top: r.top, left: r.left, width: r.width };
}),
);
const next = nextTile(rows, here, dir);
if (next === null) return;
setActive(next);
tileLink(next)?.focus();
};
if (event.key === "ArrowRight") move("right");
else if (event.key === "ArrowLeft") move("left");
else if (event.key === "ArrowDown") move("down");
else if (event.key === "ArrowUp") move("up");
else if (event.key === "Escape") {
// Through state: the grid owns the selection, and the boxes render from it.
event.preventDefault();
setSelected([]);
} else if (event.key === "x") {
// The real checkbox: the bulk form reads those, so a parallel model would select rows it never submits.
event.preventDefault();
const box = tile.querySelector("input[type=checkbox]");
if (box instanceof HTMLInputElement) box.click();
} else if (event.key === "c") {
event.preventDefault();
const button = tile.querySelector("[data-cap-copy]");
if (button instanceof HTMLButtonElement) button.click();
}
},
[setActive, setSelected],
);
return { onFocus, onKeyDown };
}