feat(ui): topbar primitives — IconButton, SegmentedControl, Avatar
Adds the shared primitives the planner topbar redesign needs, on the design-system tokens: - IconButton — icon-only button (undo/redo), ghost/secondary variants, requires an accessible label. - SegmentedControl — generic single-select toggle (color mode Plain/Elevation/Surface), radiogroup semantics. - Avatar — colored initial circle for participants, per-user color with an accent fallback. Co-located jsdom unit tests for each (roles, aria-checked, onChange, disabled, color fallback). Extends the dev-only /dev/ui gallery with sections for all three. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c9ad475e59
commit
1bd5f18a33
8 changed files with 371 additions and 2 deletions
49
packages/ui/src/SegmentedControl.test.tsx
Normal file
49
packages/ui/src/SegmentedControl.test.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
||||
import { SegmentedControl } from "./SegmentedControl.tsx";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const OPTIONS = [
|
||||
{ value: "plain", label: "Plain" },
|
||||
{ value: "elevation", label: "Elevation" },
|
||||
{ value: "surface", label: "Surface" },
|
||||
] as const;
|
||||
|
||||
describe("SegmentedControl", () => {
|
||||
it("marks the selected option as checked", () => {
|
||||
render(
|
||||
<SegmentedControl options={OPTIONS} value="elevation" onChange={() => {}} />,
|
||||
);
|
||||
expect(screen.getByRole("radio", { name: "Elevation" })).toHaveProperty(
|
||||
"ariaChecked",
|
||||
"true",
|
||||
);
|
||||
expect(screen.getByRole("radio", { name: "Plain" })).toHaveProperty(
|
||||
"ariaChecked",
|
||||
"false",
|
||||
);
|
||||
});
|
||||
|
||||
it("calls onChange with the clicked option's value", () => {
|
||||
const onChange = vi.fn();
|
||||
render(
|
||||
<SegmentedControl options={OPTIONS} value="plain" onChange={onChange} />,
|
||||
);
|
||||
fireEvent.click(screen.getByRole("radio", { name: "Surface" }));
|
||||
expect(onChange).toHaveBeenCalledWith("surface");
|
||||
});
|
||||
|
||||
it("renders a labelled radiogroup", () => {
|
||||
render(
|
||||
<SegmentedControl
|
||||
options={OPTIONS}
|
||||
value="plain"
|
||||
onChange={() => {}}
|
||||
label="Colour mode"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("radiogroup", { name: "Colour mode" })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue