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:
@@ -0,0 +1,159 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { useRowSelection } from "./useRowSelection";
|
||||
|
||||
const ABCDE = ["a", "b", "c", "d", "e"];
|
||||
|
||||
function setup(shown = ABCDE, all = ABCDE, scope: unknown = 1) {
|
||||
return renderHook(
|
||||
(p: { all: string[]; shown: string[]; scope: unknown }) =>
|
||||
useRowSelection(p.all, p.shown, p.scope),
|
||||
{ initialProps: { all, shown, scope } },
|
||||
);
|
||||
}
|
||||
|
||||
describe("useRowSelection", () => {
|
||||
it("toggles one row at a time, and says the selected rows in the order shown", () => {
|
||||
const { result } = setup();
|
||||
act(() => result.current.toggle("d"));
|
||||
act(() => result.current.toggle("b"));
|
||||
expect(result.current.shownSelected).toEqual(["b", "d"]);
|
||||
act(() => result.current.toggle("d"));
|
||||
expect(result.current.shownSelected).toEqual(["b"]);
|
||||
expect(result.current.isSelected("b")).toBe(true);
|
||||
expect(result.current.isSelected("d")).toBe(false);
|
||||
});
|
||||
|
||||
it("selects the range from the last row toggled, in either direction", () => {
|
||||
const { result } = setup();
|
||||
act(() => result.current.toggle("b"));
|
||||
act(() => result.current.toggle("d", true));
|
||||
expect(result.current.shownSelected).toEqual(["b", "c", "d"]);
|
||||
act(() => {
|
||||
result.current.clear();
|
||||
});
|
||||
act(() => result.current.toggle("d"));
|
||||
act(() => result.current.toggle("b", true));
|
||||
expect(result.current.shownSelected).toEqual(["b", "c", "d"]);
|
||||
});
|
||||
|
||||
it("takes a range's state from the row the range ends on: it can clear rows too", () => {
|
||||
const { result } = setup();
|
||||
act(() => result.current.toggle("a"));
|
||||
act(() => result.current.toggle("e", true));
|
||||
expect(result.current.shownSelected).toEqual(ABCDE);
|
||||
// `e` is selected, so a range ending on it clears; the anchor moved to `e` with the last click.
|
||||
act(() => result.current.toggle("b", true));
|
||||
expect(result.current.shownSelected).toEqual(["a"]);
|
||||
});
|
||||
|
||||
it("leaves rows outside the range alone", () => {
|
||||
const { result } = setup();
|
||||
act(() => result.current.toggle("a"));
|
||||
act(() => result.current.toggle("c"));
|
||||
act(() => result.current.toggle("e", true));
|
||||
expect(result.current.shownSelected).toEqual(["a", "c", "d", "e"]);
|
||||
});
|
||||
|
||||
it("follows the order on screen, not the stored one", () => {
|
||||
const { result } = setup(["e", "c", "a", "b", "d"]);
|
||||
act(() => result.current.toggle("c"));
|
||||
act(() => result.current.toggle("b", true));
|
||||
expect(result.current.shownSelected).toEqual(["c", "a", "b"]);
|
||||
});
|
||||
|
||||
it("makes a range with no row to start from a plain toggle", () => {
|
||||
const { result } = setup();
|
||||
act(() => result.current.toggle("c", true));
|
||||
expect(result.current.shownSelected).toEqual(["c"]);
|
||||
});
|
||||
|
||||
it("makes a range whose first row a filter hid a plain toggle", () => {
|
||||
const { result, rerender } = setup();
|
||||
act(() => result.current.toggle("a"));
|
||||
rerender({ all: ABCDE, shown: ["c", "d", "e"], scope: 1 });
|
||||
act(() => result.current.toggle("e", true));
|
||||
expect(result.current.shownSelected).toEqual(["e"]);
|
||||
});
|
||||
|
||||
it("has the header box cover the rows shown, and keep what a filter hides", () => {
|
||||
const { result, rerender } = setup();
|
||||
expect(result.current.headerState).toBe("none");
|
||||
act(() => result.current.toggle("a"));
|
||||
expect(result.current.headerState).toBe("some");
|
||||
rerender({ all: ABCDE, shown: ["c", "d"], scope: 1 });
|
||||
// The one selected row is hidden: nothing SHOWN is selected, and acting counts what is shown.
|
||||
expect(result.current.headerState).toBe("none");
|
||||
expect(result.current.shownSelected).toEqual([]);
|
||||
act(() => result.current.toggleAllShown());
|
||||
expect(result.current.headerState).toBe("all");
|
||||
expect(result.current.shownSelected).toEqual(["c", "d"]);
|
||||
act(() => result.current.toggleAllShown());
|
||||
expect(result.current.headerState).toBe("none");
|
||||
rerender({ all: ABCDE, shown: ABCDE, scope: 1 });
|
||||
expect(result.current.shownSelected).toEqual(["a"]);
|
||||
});
|
||||
|
||||
it("selects every row shown from a partial selection", () => {
|
||||
const { result } = setup();
|
||||
act(() => result.current.toggle("b"));
|
||||
act(() => result.current.toggleAllShown());
|
||||
expect(result.current.shownSelected).toEqual(ABCDE);
|
||||
});
|
||||
|
||||
it("has no header state to show over no rows", () => {
|
||||
const { result } = setup([], []);
|
||||
expect(result.current.headerState).toBe("none");
|
||||
act(() => result.current.toggleAllShown());
|
||||
expect(result.current.headerState).toBe("none");
|
||||
});
|
||||
|
||||
it("drops a row that left the list, so it does not come back selected", () => {
|
||||
const { result, rerender } = setup();
|
||||
act(() => result.current.toggle("b"));
|
||||
act(() => result.current.toggle("c"));
|
||||
rerender({ all: ["a", "c", "d", "e"], shown: ["a", "c", "d", "e"], scope: 1 });
|
||||
expect(result.current.shownSelected).toEqual(["c"]);
|
||||
rerender({ all: ABCDE, shown: ABCDE, scope: 1 });
|
||||
expect(result.current.shownSelected).toEqual(["c"]);
|
||||
});
|
||||
|
||||
it("clears on a change of scope, in the render that changes it", () => {
|
||||
const { result, rerender } = setup();
|
||||
act(() => result.current.toggle("a"));
|
||||
// The same videos in another list are not selected there.
|
||||
rerender({ all: ABCDE, shown: ABCDE, scope: 2 });
|
||||
expect(result.current.shownSelected).toEqual([]);
|
||||
act(() => result.current.toggle("c", true));
|
||||
expect(result.current.shownSelected).toEqual(["c"]);
|
||||
});
|
||||
|
||||
it("says whether clearing did anything", () => {
|
||||
const { result } = setup();
|
||||
let cleared = true;
|
||||
act(() => {
|
||||
cleared = result.current.clear();
|
||||
});
|
||||
expect(cleared).toBe(false);
|
||||
act(() => result.current.toggle("a"));
|
||||
act(() => {
|
||||
cleared = result.current.clear();
|
||||
});
|
||||
expect(cleared).toBe(true);
|
||||
expect(result.current.shownSelected).toEqual([]);
|
||||
});
|
||||
|
||||
it("counts a selected row a filter hides as something to clear", () => {
|
||||
const { result, rerender } = setup();
|
||||
act(() => result.current.toggle("a"));
|
||||
rerender({ all: ABCDE, shown: ["c"], scope: 1 });
|
||||
let cleared = false;
|
||||
act(() => {
|
||||
cleared = result.current.clear();
|
||||
});
|
||||
expect(cleared).toBe(true);
|
||||
rerender({ all: ABCDE, shown: ABCDE, scope: 1 });
|
||||
expect(result.current.shownSelected).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user