The second half of the button sweep: the remaining hand-rolled buttons in
feature components, the design lab and the components/ui composites now
stand on Button, IconButton, Pressable, Chip or DragHandle.
- Chip + DismissibleChip (filter / token / tag / suggest / ghost, 24px
floor, 44 on touch) replace five chip geometries; FilterChipGroup
renders Chips; overflow lanes measure a mirror painted by chipClass.
- The Plex control row is IconButton's player rung (36, 20px glyph,
round, overlay; 44 on touch); the YT modal bar is on the sm rung with
pressed cycle toggles and names that survive a folded label.
- New variants: IconButton ghost-accent, danger-soft; Button warn on a
new state-warn utility. The .btn-{tone} recipes and btnCls are gone.
- Fixed on the way: membership rows (collection editor, Plex playlist
add) announce checked/mixed; a one-letter tag chip was 21px wide;
descriptionLinks moved out of lib/ (it renders components).
- local/button-primitive: a raw <button>/<FocusSafeButton> outside the
control-primitive files is a lint error (contract-tested, seen red).
87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
// @vitest-environment jsdom
|
||
//
|
||
// E28 S3b: the chip family (docs/DESIGN.md "One primitive per button family"). One geometry for a
|
||
// filter, an applied narrowing, a tag and a suggestion, where five files had five.
|
||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||
import { Chip, DismissibleChip } from "./Chip";
|
||
import { chipClass } from "./chipStyles";
|
||
|
||
afterEach(cleanup);
|
||
|
||
describe("Chip", () => {
|
||
it("is a pill on the 24px floor that never submits a form, and a toggle announces its state", () => {
|
||
const onSubmit = vi.fn((e: { preventDefault: () => void }) => e.preventDefault());
|
||
render(
|
||
<form onSubmit={onSubmit}>
|
||
<Chip pressed={false}>2024</Chip>
|
||
</form>,
|
||
);
|
||
const chip = screen.getByRole("button", { name: "2024" });
|
||
expect(chip.className).toContain("rounded-full");
|
||
expect(chip.className).toContain("h-6");
|
||
expect(chip.className).toContain("touch-target");
|
||
expect(chip.getAttribute("aria-pressed")).toBe("false");
|
||
fireEvent.click(chip);
|
||
expect(onSubmit).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("a removing chip is named by the removal and shows the ×", () => {
|
||
const onClick = vi.fn();
|
||
render(
|
||
<Chip variant="token" size="md" removeLabel="Remove Music" onClick={onClick}>
|
||
Music
|
||
</Chip>,
|
||
);
|
||
const chip = screen.getByRole("button", { name: "Remove Music" });
|
||
expect(chip.getAttribute("title")).toBe("Remove Music");
|
||
expect(chip.className).toContain("h-7");
|
||
expect(chip.querySelector("svg")).not.toBeNull();
|
||
fireEvent.click(chip);
|
||
expect(onClick).toHaveBeenCalledOnce();
|
||
});
|
||
|
||
it("the measuring mirror lays out exactly like the chip it measures", () => {
|
||
render(
|
||
<Chip variant="tag" size="xs">
|
||
tag
|
||
</Chip>,
|
||
);
|
||
const cls = screen.getByRole("button", { name: "tag" }).className;
|
||
for (const c of chipClass("tag", "xs").split(" ")) expect(cls).toContain(c);
|
||
});
|
||
});
|
||
|
||
describe("DismissibleChip", () => {
|
||
it("is two sibling controls at most, never a button in a button", () => {
|
||
const onAccept = vi.fn();
|
||
const onDismiss = vi.fn();
|
||
const { container } = render(
|
||
<DismissibleChip
|
||
variant="suggest"
|
||
onClick={onAccept}
|
||
bodyLabel="Add the tag Music"
|
||
onDismiss={onDismiss}
|
||
dismissLabel="Not now"
|
||
>
|
||
Music
|
||
</DismissibleChip>,
|
||
);
|
||
expect(container.querySelector("button button")).toBeNull();
|
||
fireEvent.click(screen.getByRole("button", { name: "Add the tag Music" }));
|
||
fireEvent.click(screen.getByRole("button", { name: "Not now" }));
|
||
expect(onAccept).toHaveBeenCalledOnce();
|
||
expect(onDismiss).toHaveBeenCalledOnce();
|
||
});
|
||
|
||
it("with no body action the label is text, and only the × is a control", () => {
|
||
render(
|
||
<DismissibleChip onDismiss={() => {}} dismissLabel="Remove Year: 2024">
|
||
Year: 2024
|
||
</DismissibleChip>,
|
||
);
|
||
expect(screen.getAllByRole("button")).toHaveLength(1);
|
||
expect(screen.getByText("Year: 2024").closest("button")).toBeNull();
|
||
});
|
||
});
|