feat(files): a press anywhere on a file's row plays it

A table row (and its narrow card) can carry its object's default action:
a click or tap on the row runs it, a press on a control inside the row,
the click that ends a text selection and a drag do not.
This commit is contained in:
2026-11-01 03:30:49 +01:00
parent 85fd9b6704
commit 41052eb4ea
5 changed files with 135 additions and 5 deletions
@@ -1390,3 +1390,61 @@ describe("Files · a file watched part way", () => {
expect(await screen.findByText("playing 3")).toBeTruthy();
});
});
// The row is the click (and tap) surface for its file's default action, as the name always was: a
// press anywhere on it plays. A press on one of its own controls is that control's, and a click that
// ends a selection or a drag across the row opens nothing (DESIGN "Text stays selectable").
describe("Files · a press on the row plays it", () => {
const sizeCellOf = (text: string) =>
[...rowOf(text).querySelectorAll<HTMLElement>("td")].find((td) => !td.querySelector("button"))!;
it("plays the file from a click on the row, not only on its name", async () => {
mocks.files = [file(1)];
mocks.folders = [YT];
mount();
fireEvent.click(sizeCellOf("Clip 1"));
expect(await screen.findByText("playing 1")).toBeTruthy();
});
it("leaves a press on the row's own control to that control", async () => {
mocks.files = [file(1)];
mocks.folders = [YT];
mount();
// Share opens its own dialog; the row must not start the player under it.
const share = rowOf("Clip 1").querySelector<HTMLElement>(
'button[aria-label="files.actions.share"]',
)!;
fireEvent.click(share);
expect(await screen.findByRole("dialog")).toBeTruthy();
expect(screen.queryByText("playing 1")).toBeNull();
});
it("opens nothing for a press that travelled across the row", async () => {
mocks.files = [file(1)];
mocks.folders = [YT];
mount();
const cell = sizeCellOf("Clip 1");
// jsdom has no PointerEvent: the press's coordinates are set on the event by hand.
const down = createEvent.pointerDown(cell);
Object.defineProperties(down, { clientX: { value: 10 }, clientY: { value: 10 } });
fireEvent(cell, down);
fireEvent.click(cell, { clientX: 60, clientY: 10, detail: 1 });
await act(async () => {});
expect(screen.queryByText("playing 1")).toBeNull();
});
it("opens nothing for the click that ends selecting the row's words", async () => {
mocks.files = [file(1)];
mocks.folders = [YT];
mount();
const cell = sizeCellOf("Clip 1");
const range = document.createRange();
range.selectNodeContents(rowOf("Clip 1"));
window.getSelection()!.removeAllRanges();
window.getSelection()!.addRange(range);
fireEvent.click(cell, { detail: 1 });
await act(async () => {});
window.getSelection()!.removeAllRanges();
expect(screen.queryByText("playing 1")).toBeNull();
});
});
+2
View File
@@ -690,6 +690,8 @@ export default function Files() {
narrowedBy={q}
pageScope={pageScope}
rowProps={(f) => ({ ...rowMenu.triggerProps(f), "data-file-id": f.id })}
// A press anywhere on the row does what its name does: plays, or opens the viewer.
rowOpen={openOf}
// `group`: the row's quick icons show while it is hovered or holds the focus.
rowClassName={(f) =>
ringed === f.id ? "group bg-accent/8 ring-2 ring-inset ring-accent/60" : "group"