endsTextSelection takes the click event and exempts a keyboard click (detail 0): Enter / Space ends no selection, so a title selected and then tabbed to opened nothing. All three callers (FocusSafeButton, the Plex caption, the video card title) go through it.
100 lines
3.7 KiB
TypeScript
100 lines
3.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// docs/DESIGN.md "Text stays selectable": a button whose text is a title or a name is `selectable` —
|
|
// its text selects (Chrome selects none inside a button without `user-select: text`), and the click
|
|
// that ends selecting it, or a press dragged past the slop, does nothing. Held here, on the primitive every button family is built on.
|
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
|
import { Pressable } from "./Pressable";
|
|
import { Button } from "./Button";
|
|
|
|
afterEach(() => {
|
|
window.getSelection()!.removeAllRanges();
|
|
cleanup();
|
|
});
|
|
|
|
function selectText(el: Element) {
|
|
const range = document.createRange();
|
|
range.selectNodeContents(el);
|
|
window.getSelection()!.removeAllRanges();
|
|
window.getSelection()!.addRange(range);
|
|
}
|
|
|
|
describe("a selectable button", () => {
|
|
it("lets its text select, and ignores the click that ends a selection", () => {
|
|
const onClick = vi.fn();
|
|
render(
|
|
<Pressable selectable onClick={onClick}>
|
|
<span>Andor</span>
|
|
</Pressable>,
|
|
);
|
|
const btn = screen.getByRole("button");
|
|
expect(btn.classList.contains("select-text")).toBe(true);
|
|
selectText(screen.getByText("Andor"));
|
|
fireEvent.click(btn, { detail: 1 }); // the mouseup that ended the drag-select
|
|
expect(onClick).not.toHaveBeenCalled();
|
|
window.getSelection()!.removeAllRanges();
|
|
fireEvent.click(btn, { detail: 1 });
|
|
expect(onClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
// E28 review H2: Enter / Space on a focused title with its words still selected (selected, then
|
|
// tabbed to) did nothing — a keyboard click ends no selection; it carries no pointer at all.
|
|
it("a keyboard click opens even with the title's words selected", () => {
|
|
const onClick = vi.fn();
|
|
render(
|
|
<Pressable selectable onClick={onClick}>
|
|
<span>Andor</span>
|
|
</Pressable>,
|
|
);
|
|
selectText(screen.getByText("Andor"));
|
|
fireEvent.click(screen.getByRole("button"), { detail: 0 });
|
|
expect(onClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("ignores a press dragged past the slop, keeps a steady one and a keyboard click", () => {
|
|
const onClick = vi.fn();
|
|
render(
|
|
<Pressable selectable onClick={onClick}>
|
|
Andor
|
|
</Pressable>,
|
|
);
|
|
const btn = screen.getByRole("button");
|
|
fireEvent.mouseDown(btn, { clientX: 10, clientY: 10 });
|
|
fireEvent.click(btn, { detail: 1, clientX: 40, clientY: 10 });
|
|
expect(onClick).not.toHaveBeenCalled();
|
|
fireEvent.mouseDown(btn, { clientX: 10, clientY: 10 });
|
|
fireEvent.click(btn, { detail: 1, clientX: 13, clientY: 12 });
|
|
expect(onClick).toHaveBeenCalledTimes(1);
|
|
// Enter / Space: a click with no pointer, wherever the last press was.
|
|
fireEvent.mouseDown(btn, { clientX: 10, clientY: 10 });
|
|
fireEvent.click(btn, { detail: 0, clientX: 0, clientY: 0 });
|
|
expect(onClick).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("still clicks when the selection is somewhere else on the page", () => {
|
|
const onClick = vi.fn();
|
|
render(
|
|
<>
|
|
<p>elsewhere</p>
|
|
<Button variant="link-muted" selectable onClick={onClick}>
|
|
Andor
|
|
</Button>
|
|
</>,
|
|
);
|
|
selectText(screen.getByText("elsewhere"));
|
|
screen.getByRole("button").click();
|
|
expect(onClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("is off unless asked: a plain button neither selects nor guards", () => {
|
|
const onClick = vi.fn();
|
|
render(<Pressable onClick={onClick}>Save</Pressable>);
|
|
const btn = screen.getByRole("button");
|
|
expect(btn.classList.contains("select-text")).toBe(false);
|
|
selectText(btn);
|
|
btn.click();
|
|
expect(onClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|