- Playlist rows: the X column became the row's "…" (play, pin, copy title/link, move, order,
remove). Remove runs at once, with no confirm; its toast's Undo puts the row back at its index.
The Delete key removes a keyboard-walked row, and focus moves on to the next.
- POST /api/playlists/{id}/items takes an optional `position`, so an undo is one request that
renumbers the list around the row.
- A new `listDelete` key scope. Only a list with `onDelete` registers it, so the help sheet does not
offer Delete where nothing handles it.
- A playlist has one list, `usePlaylistActions.items`: rename, pin, YouTube open/push/revert,
delete. The rail row's "…" and the page header's "…" both open it. Push and revert moved into
the hook; the header keeps Play all.
- Phone-width playlist table: "#" and Channel are hidden below sm (`Column.phoneHidden`). Their
fixed widths had left the title no width.
67 lines
2.4 KiB
TypeScript
67 lines
2.4 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// The `list` key scope — what puts j/k, the arrows and Enter on the cheat sheet — is live only while a
|
|
// registered list has an item to land on. An empty download queue, a list still loading or one that
|
|
// failed mounts its container all the same, and the sheet listed the keys over nothing (deep-review
|
|
// E11 round 2). Its own file: listNav keeps module state.
|
|
import { useRef } from "react";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
import { navItemAttrs, useListNav } from "./listNav";
|
|
import { useActiveScopes } from "./keymap";
|
|
|
|
afterEach(cleanup);
|
|
|
|
function Scopes() {
|
|
return <output data-testid="scopes">{useActiveScopes().join(" ")}</output>;
|
|
}
|
|
|
|
function Queue({ count, onDelete }: { count: number; onDelete?: (i: number) => void }) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
useListNav(ref, { enabled: true, onDelete });
|
|
return (
|
|
<div ref={ref}>
|
|
{count === 0 ? (
|
|
<p>Nothing queued</p>
|
|
) : (
|
|
Array.from({ length: count }, (_, i) => (
|
|
<div key={i} {...navItemAttrs(i)}>
|
|
item
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const page = (count: number, onDelete?: (i: number) => void) => (
|
|
<>
|
|
<Scopes />
|
|
<Queue count={count} onDelete={onDelete} />
|
|
</>
|
|
);
|
|
const scopes = () => screen.getByTestId("scopes").textContent!.split(" ");
|
|
|
|
describe("useListNav · the list key scope", () => {
|
|
it("is not live over a list with nothing in it, and is while it has items", async () => {
|
|
const { rerender } = render(page(0));
|
|
expect(scopes()).not.toContain("list");
|
|
rerender(page(2));
|
|
await waitFor(() => expect(scopes()).toContain("list"));
|
|
rerender(page(0));
|
|
await waitFor(() => expect(scopes()).not.toContain("list"));
|
|
});
|
|
|
|
// E28 S3e2: Delete is on the sheet only where a list can take an item out — a playlist's rows, not
|
|
// the feed or a Plex grid, where nothing answers it.
|
|
it("puts Delete on the sheet only for a list that removes items", async () => {
|
|
const { rerender } = render(page(2));
|
|
await waitFor(() => expect(scopes()).toContain("list"));
|
|
expect(scopes()).not.toContain("listDelete");
|
|
rerender(page(2, () => {}));
|
|
await waitFor(() => expect(scopes()).toContain("listDelete"));
|
|
rerender(page(0, () => {}));
|
|
await waitFor(() => expect(scopes()).not.toContain("listDelete"));
|
|
});
|
|
});
|