feat(playlists): select rows with a box, Shift for a range, x and Esc
A box first in every row and one over the column for the rows shown, the selected row tinted. A filter keeps what it hides selected; another list starts over; a row that left the list drops out. Watch later has none. A box no longer counts as a typing target: a click on one left the focus there and the page's keys went dead.
This commit is contained in:
@@ -33,6 +33,7 @@ export type KeyScope =
|
||||
| "search"
|
||||
| "list"
|
||||
| "listDelete"
|
||||
| "listSelect"
|
||||
| "undo"
|
||||
| "contextMenu"
|
||||
| "ytPlayer"
|
||||
@@ -46,6 +47,7 @@ const PAGE_LEVEL: ReadonlySet<KeyScope> = new Set<KeyScope>([
|
||||
"search",
|
||||
"list",
|
||||
"listDelete",
|
||||
"listSelect",
|
||||
]);
|
||||
/** True for a scope that acts on the page beneath the overlays (see `KeyScope`). */
|
||||
export const isPageLevelScope = (scope: KeyScope): boolean => PAGE_LEVEL.has(scope);
|
||||
@@ -138,6 +140,15 @@ export const SHORTCUTS = [
|
||||
// Its own scope beside `list`: only a list that can take an item out (a playlist's rows) registers
|
||||
// it, so the sheet does not promise Delete on the feed or a Plex grid, where nothing answers it.
|
||||
{ id: "remove", scope: "listDelete", keys: ["Delete"], labelKey: "keys.listRemove" },
|
||||
// …and only a list whose rows can be selected registers these. Escape stays quiet in a field: the
|
||||
// field's own Escape comes first, and a selection is not what the reader is in while typing.
|
||||
{ id: "select", scope: "listSelect", keys: ["x"], labelKey: "keys.listSelect" },
|
||||
{
|
||||
id: "clearSelection",
|
||||
scope: "listSelect",
|
||||
keys: ["Escape"],
|
||||
labelKey: "keys.listClearSelection",
|
||||
},
|
||||
// --- the row context menu (wherever useContextMenu is wired) ---------------------------------
|
||||
// Its OWN scope, not `global`: only the surfaces that actually call `useContextMenu` register it,
|
||||
// so the sheet stops advertising "Open the context menu" on the feed, Plex, Downloads and Settings
|
||||
@@ -264,15 +275,22 @@ export function isTypingTarget(e: KeyboardEvent): boolean {
|
||||
const el = (e.target as HTMLElement | null) ?? (document.activeElement as HTMLElement | null);
|
||||
if (!el) return false;
|
||||
const tag = (el.tagName || "").toLowerCase();
|
||||
// A box takes no letters, and a click on a table row's box leaves the focus on it: `j` went dead
|
||||
// there. A slider stays a field, as its arrows are its own.
|
||||
if (isBox(el)) return false;
|
||||
return tag === "input" || tag === "textarea" || tag === "select" || !!el.isContentEditable;
|
||||
}
|
||||
|
||||
function isBox(el: HTMLElement | null): boolean {
|
||||
return el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio");
|
||||
}
|
||||
|
||||
/** True while focus is on something Space/Enter activates natively. Both players defer Space to a
|
||||
* focused button ("Skip intro") rather than toggling playback; this is that shared rule. */
|
||||
export function isActivationTarget(e: KeyboardEvent): boolean {
|
||||
const el = (e.target as HTMLElement | null) ?? (document.activeElement as HTMLElement | null);
|
||||
const tag = (el?.tagName || "").toLowerCase();
|
||||
return tag === "button" || tag === "a";
|
||||
return tag === "button" || tag === "a" || isBox(el);
|
||||
}
|
||||
|
||||
/** One printable character that is neither a letter nor a digit (`[`, `]`, `,`). */
|
||||
@@ -462,7 +480,7 @@ export function useActiveScopes(): KeyScope[] {
|
||||
// registers `page` in its own effect, which React runs after every child's — so by mount order
|
||||
// the module jumps would head the sheet, above the list keys of the page you are looking at.
|
||||
const surfaces = live.filter((s) => !isPageLevelScope(s));
|
||||
const pageLevel = (["list", "listDelete", "search", "page"] as const).filter((s) =>
|
||||
const pageLevel = (["list", "listSelect", "listDelete", "search", "page"] as const).filter((s) =>
|
||||
live.includes(s),
|
||||
);
|
||||
return [...surfaces, ...pageLevel, "global"];
|
||||
|
||||
Reference in New Issue
Block a user