@primitiv-ui/react 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/Checkbox/Checkbox.tsx +96 -120
- package/src/Checkbox/CheckboxContext.ts +8 -0
- package/src/Checkbox/README.md +69 -88
- package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +48 -10
- package/src/Checkbox/__tests__/Checkbox.contract.test.tsx +29 -40
- package/src/Checkbox/__tests__/Checkbox.controlled-state.test.tsx +16 -24
- package/src/Checkbox/__tests__/Checkbox.disabled.test.tsx +33 -3
- package/src/Checkbox/__tests__/Checkbox.form.test.tsx +53 -0
- package/src/Checkbox/__tests__/Checkbox.indeterminate.test.tsx +28 -34
- package/src/Checkbox/__tests__/Checkbox.indicator.test.tsx +27 -52
- package/src/Checkbox/__tests__/Checkbox.uncontrolled-state.test.tsx +33 -17
- package/src/Checkbox/hooks/index.ts +1 -0
- package/src/Checkbox/hooks/useCheckboxInput.ts +49 -0
- package/src/Checkbox/types.ts +24 -17
- package/src/Radio/README.md +89 -86
- package/src/Radio/Radio.tsx +87 -98
- package/src/Radio/RadioContext.ts +9 -0
- package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +34 -9
- package/src/Radio/__tests__/Radio.contract.test.tsx +30 -36
- package/src/Radio/__tests__/Radio.controlled-state.test.tsx +21 -11
- package/src/Radio/__tests__/Radio.disabled.test.tsx +31 -6
- package/src/Radio/__tests__/Radio.native-grouping.test.tsx +77 -0
- package/src/Radio/__tests__/Radio.parts.test.tsx +76 -0
- package/src/Radio/__tests__/Radio.uncontrolled-state.test.tsx +39 -32
- package/src/Radio/hooks/useRadioRoot.ts +32 -16
- package/src/Radio/types.ts +30 -16
- package/src/Switch/README.md +45 -57
- package/src/Switch/Switch.tsx +78 -65
- package/src/Switch/SwitchContext.ts +3 -1
- package/src/Switch/__tests__/Switch.asChild.test.tsx +16 -34
- package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +47 -28
- package/src/Switch/__tests__/Switch.contract.test.tsx +21 -16
- package/src/Switch/__tests__/Switch.controlled-state.test.tsx +10 -12
- package/src/Switch/__tests__/Switch.disabled.test.tsx +12 -17
- package/src/Switch/__tests__/Switch.form.test.tsx +55 -0
- package/src/Switch/__tests__/Switch.keyboard-interaction.test.tsx +4 -4
- package/src/Switch/__tests__/Switch.uncontrolled-state.test.tsx +55 -20
- package/src/Switch/hooks/useSwitchRoot.ts +29 -11
- package/src/Switch/types.ts +20 -10
- package/src/Checkbox/__tests__/Checkbox.asChild.test.tsx +0 -69
- package/src/Radio/__tests__/Radio.asChild.test.tsx +0 -63
- package/src/Radio/__tests__/Radio.indicator.test.tsx +0 -103
|
@@ -1,41 +1,79 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
1
2
|
import { render, screen } from "@testing-library/react";
|
|
2
3
|
|
|
3
4
|
import { Checkbox } from "../Checkbox";
|
|
4
5
|
|
|
5
6
|
describe("Checkbox basic rendering", () => {
|
|
6
|
-
it(
|
|
7
|
+
it("renders a real native <input type=checkbox> as the control", () => {
|
|
7
8
|
// Arrange & Act
|
|
8
9
|
render(<Checkbox.Root aria-label="Accept terms" />);
|
|
9
10
|
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
10
11
|
|
|
11
12
|
// Assert
|
|
12
|
-
expect(checkbox.tagName).toBe("
|
|
13
|
+
expect(checkbox.tagName).toBe("INPUT");
|
|
14
|
+
expect(checkbox).toHaveAttribute("type", "checkbox");
|
|
13
15
|
});
|
|
14
16
|
|
|
15
|
-
it(
|
|
17
|
+
it("wraps the input in a <label> so the visible box is clickable", () => {
|
|
16
18
|
// Arrange & Act
|
|
17
19
|
render(<Checkbox.Root aria-label="Accept terms" />);
|
|
18
|
-
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
19
20
|
|
|
20
21
|
// Assert
|
|
21
|
-
expect(
|
|
22
|
+
expect(
|
|
23
|
+
screen.getByRole("checkbox", { name: "Accept terms" }).closest("label"),
|
|
24
|
+
).not.toBeNull();
|
|
22
25
|
});
|
|
23
26
|
|
|
24
|
-
it(
|
|
27
|
+
it("is unchecked by default", () => {
|
|
25
28
|
// Arrange & Act
|
|
26
29
|
render(<Checkbox.Root aria-label="Accept terms" />);
|
|
30
|
+
|
|
31
|
+
// Assert
|
|
32
|
+
expect(screen.getByRole("checkbox", { name: "Accept terms" })).not.toBeChecked();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("forwards native input attributes (name, value, id) to the input", () => {
|
|
36
|
+
// Arrange & Act
|
|
37
|
+
render(
|
|
38
|
+
<Checkbox.Root id="c1" name="terms" value="accepted" aria-label="Accept terms" />,
|
|
39
|
+
);
|
|
27
40
|
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
28
41
|
|
|
29
42
|
// Assert
|
|
30
|
-
expect(checkbox).toHaveAttribute("
|
|
43
|
+
expect(checkbox).toHaveAttribute("id", "c1");
|
|
44
|
+
expect(checkbox).toHaveAttribute("name", "terms");
|
|
45
|
+
expect(checkbox).toHaveAttribute("value", "accepted");
|
|
31
46
|
});
|
|
32
47
|
|
|
33
|
-
it(
|
|
48
|
+
it("applies className to the box, not the hidden input", () => {
|
|
34
49
|
// Arrange & Act
|
|
35
|
-
render(<Checkbox.Root aria-label="Accept terms" />);
|
|
50
|
+
render(<Checkbox.Root className="box" aria-label="Accept terms" />);
|
|
36
51
|
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
37
52
|
|
|
38
53
|
// Assert
|
|
39
|
-
expect(checkbox).
|
|
54
|
+
expect(checkbox).not.toHaveClass("box");
|
|
55
|
+
expect(checkbox.closest("label")).toHaveClass("box");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("forwards a ref to the underlying input", () => {
|
|
59
|
+
// Arrange
|
|
60
|
+
const ref = createRef<HTMLInputElement>();
|
|
61
|
+
|
|
62
|
+
// Act
|
|
63
|
+
render(<Checkbox.Root ref={ref} aria-label="Accept terms" />);
|
|
64
|
+
|
|
65
|
+
// Assert
|
|
66
|
+
expect(ref.current).toBe(screen.getByRole("checkbox", { name: "Accept terms" }));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('sets data-state="unchecked" on the box when unchecked', () => {
|
|
70
|
+
// Arrange & Act
|
|
71
|
+
const { container } = render(<Checkbox.Root aria-label="Accept terms" />);
|
|
72
|
+
|
|
73
|
+
// Assert
|
|
74
|
+
expect(container.querySelector("label")).toHaveAttribute(
|
|
75
|
+
"data-state",
|
|
76
|
+
"unchecked",
|
|
77
|
+
);
|
|
40
78
|
});
|
|
41
79
|
});
|
|
@@ -2,19 +2,21 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
|
|
5
|
-
import { render
|
|
5
|
+
import { render } from "@testing-library/react";
|
|
6
6
|
|
|
7
7
|
import { Checkbox } from "../Checkbox";
|
|
8
|
+
import { CheckedState } from "../types";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Auto-verification of the Checkbox styling contract (RFC 0004 §3.4 / D15). The
|
|
11
12
|
* `data-*` half of `registry/components/checkbox/contract.json` is *derived from
|
|
12
13
|
* and asserted against the rendered headless component* so it can never drift
|
|
13
|
-
* from what the component actually emits.
|
|
14
|
-
* `data-state="checked" |
|
|
15
|
-
* `data-disabled=""` when
|
|
16
|
-
*
|
|
17
|
-
* checked by the generator
|
|
14
|
+
* from what the component actually emits. The checkbox's `data-*` surface lives
|
|
15
|
+
* on the **wrapper box** (the styled root) — `data-state="checked" |
|
|
16
|
+
* "unchecked" | "indeterminate"` (always present) plus `data-disabled=""` when
|
|
17
|
+
* disabled. The authored half (classes, parts, custom properties) is a styling
|
|
18
|
+
* convention the headless layer does not emit and is checked by the generator
|
|
19
|
+
* drift-guards instead.
|
|
18
20
|
*/
|
|
19
21
|
const contractPath = resolve(
|
|
20
22
|
dirname(fileURLToPath(import.meta.url)),
|
|
@@ -23,6 +25,13 @@ const contractPath = resolve(
|
|
|
23
25
|
|
|
24
26
|
const contract = JSON.parse(readFileSync(contractPath, "utf8"));
|
|
25
27
|
|
|
28
|
+
/** The wrapper element is the contracted styling root. */
|
|
29
|
+
function rootOf(container: HTMLElement): HTMLElement {
|
|
30
|
+
const root = container.querySelector("label");
|
|
31
|
+
if (!root) throw new Error("expected a Checkbox wrapper element");
|
|
32
|
+
return root;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
/** Every `data-*` attribute name present on an element. */
|
|
27
36
|
function dataAttributeNames(element: Element): string[] {
|
|
28
37
|
return [...element.attributes]
|
|
@@ -37,30 +46,23 @@ describe("Checkbox styling contract", () => {
|
|
|
37
46
|
);
|
|
38
47
|
|
|
39
48
|
it("declares every data-* attribute as auto-verified against the component", () => {
|
|
40
|
-
// The Checkbox has no authored data-* surface; the whole contract is derived.
|
|
41
49
|
expect(autoAttributes).toHaveLength(contract.dataAttributes.length);
|
|
42
50
|
});
|
|
43
51
|
|
|
44
52
|
it("emits exactly the contracted data-* attribute names across its states", () => {
|
|
45
53
|
const emitted = new Set<string>();
|
|
46
54
|
for (const ui of [
|
|
47
|
-
<Checkbox.Root key="checked" checked onCheckedChange={() => {}} aria-label="x"
|
|
48
|
-
<Checkbox.Indicator />
|
|
49
|
-
</Checkbox.Root>,
|
|
55
|
+
<Checkbox.Root key="checked" checked onCheckedChange={() => {}} aria-label="x" />,
|
|
50
56
|
<Checkbox.Root
|
|
51
57
|
key="indeterminate"
|
|
52
58
|
checked="indeterminate"
|
|
53
59
|
onCheckedChange={() => {}}
|
|
54
60
|
aria-label="x"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
</Checkbox.Root>,
|
|
58
|
-
<Checkbox.Root key="disabled" disabled aria-label="x">
|
|
59
|
-
<Checkbox.Indicator />
|
|
60
|
-
</Checkbox.Root>,
|
|
61
|
+
/>,
|
|
62
|
+
<Checkbox.Root key="disabled" disabled aria-label="x" />,
|
|
61
63
|
]) {
|
|
62
|
-
const { unmount } = render(ui);
|
|
63
|
-
for (const name of dataAttributeNames(
|
|
64
|
+
const { container, unmount } = render(ui);
|
|
65
|
+
for (const name of dataAttributeNames(rootOf(container))) {
|
|
64
66
|
emitted.add(name);
|
|
65
67
|
}
|
|
66
68
|
unmount();
|
|
@@ -78,16 +80,12 @@ describe("Checkbox styling contract", () => {
|
|
|
78
80
|
);
|
|
79
81
|
|
|
80
82
|
for (const entry of stateEntries) {
|
|
81
|
-
const checked =
|
|
82
|
-
entry.value === "indeterminate"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const { unmount } = render(
|
|
86
|
-
<Checkbox.Root checked={checked} onCheckedChange={() => {}} aria-label="x">
|
|
87
|
-
<Checkbox.Indicator />
|
|
88
|
-
</Checkbox.Root>,
|
|
83
|
+
const checked: CheckedState =
|
|
84
|
+
entry.value === "indeterminate" ? "indeterminate" : entry.value === "checked";
|
|
85
|
+
const { container, unmount } = render(
|
|
86
|
+
<Checkbox.Root checked={checked} onCheckedChange={() => {}} aria-label="x" />,
|
|
89
87
|
);
|
|
90
|
-
expect(
|
|
88
|
+
expect(rootOf(container)).toHaveAttribute("data-state", entry.value);
|
|
91
89
|
unmount();
|
|
92
90
|
}
|
|
93
91
|
});
|
|
@@ -97,21 +95,12 @@ describe("Checkbox styling contract", () => {
|
|
|
97
95
|
(attribute: { name: string }) => attribute.name === "data-disabled",
|
|
98
96
|
);
|
|
99
97
|
|
|
100
|
-
render(
|
|
101
|
-
|
|
102
|
-
<Checkbox.Indicator />
|
|
103
|
-
</Checkbox.Root>,
|
|
104
|
-
);
|
|
105
|
-
expect(screen.getByRole("checkbox")).toHaveAttribute(entry.name, entry.value);
|
|
98
|
+
const { container } = render(<Checkbox.Root disabled aria-label="x" />);
|
|
99
|
+
expect(rootOf(container)).toHaveAttribute(entry.name, entry.value);
|
|
106
100
|
});
|
|
107
101
|
|
|
108
102
|
it("omits data-disabled when the documented condition does not hold", () => {
|
|
109
|
-
render(
|
|
110
|
-
|
|
111
|
-
<Checkbox.Indicator />
|
|
112
|
-
</Checkbox.Root>,
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
expect(screen.getByRole("checkbox")).not.toHaveAttribute("data-disabled");
|
|
103
|
+
const { container } = render(<Checkbox.Root aria-label="x" />);
|
|
104
|
+
expect(rootOf(container)).not.toHaveAttribute("data-disabled");
|
|
116
105
|
});
|
|
117
106
|
});
|
|
@@ -7,27 +7,22 @@ import { Checkbox } from "../Checkbox";
|
|
|
7
7
|
describe("Checkbox controlled state", () => {
|
|
8
8
|
it("reflects the controlled `checked` prop", () => {
|
|
9
9
|
// Arrange & Act
|
|
10
|
-
const { rerender } = render(
|
|
11
|
-
<Checkbox.Root
|
|
12
|
-
checked={false}
|
|
13
|
-
onCheckedChange={() => {}}
|
|
14
|
-
aria-label="Accept terms"
|
|
15
|
-
/>,
|
|
10
|
+
const { container, rerender } = render(
|
|
11
|
+
<Checkbox.Root checked={false} onCheckedChange={() => {}} aria-label="Accept terms" />,
|
|
16
12
|
);
|
|
17
13
|
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
18
|
-
expect(checkbox).
|
|
14
|
+
expect(checkbox).not.toBeChecked();
|
|
19
15
|
|
|
20
16
|
rerender(
|
|
21
|
-
<Checkbox.Root
|
|
22
|
-
checked
|
|
23
|
-
onCheckedChange={() => {}}
|
|
24
|
-
aria-label="Accept terms"
|
|
25
|
-
/>,
|
|
17
|
+
<Checkbox.Root checked onCheckedChange={() => {}} aria-label="Accept terms" />,
|
|
26
18
|
);
|
|
27
19
|
|
|
28
20
|
// Assert
|
|
29
|
-
expect(checkbox).
|
|
30
|
-
expect(
|
|
21
|
+
expect(checkbox).toBeChecked();
|
|
22
|
+
expect(container.querySelector("label")).toHaveAttribute(
|
|
23
|
+
"data-state",
|
|
24
|
+
"checked",
|
|
25
|
+
);
|
|
31
26
|
});
|
|
32
27
|
|
|
33
28
|
it("does not update its rendered state when the parent refuses to update `checked`", async () => {
|
|
@@ -46,20 +41,17 @@ describe("Checkbox controlled state", () => {
|
|
|
46
41
|
// Act
|
|
47
42
|
await user.click(checkbox);
|
|
48
43
|
|
|
49
|
-
// Assert: callback fired but the rendered state stays
|
|
50
|
-
// parent did not flip the controlled prop.
|
|
44
|
+
// Assert: callback fired but the rendered state stays unchecked.
|
|
51
45
|
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
52
|
-
expect(checkbox).
|
|
53
|
-
expect(checkbox).toHaveAttribute("data-state", "unchecked");
|
|
46
|
+
expect(checkbox).not.toBeChecked();
|
|
54
47
|
});
|
|
55
48
|
|
|
56
49
|
it("lets a parent drive the value end to end", async () => {
|
|
57
50
|
// Arrange
|
|
58
51
|
const user = userEvent.setup();
|
|
59
52
|
function Harness() {
|
|
60
|
-
// Start true so the pre-click state
|
|
61
|
-
//
|
|
62
|
-
// defaultChecked=false and render the opposite).
|
|
53
|
+
// Start true so the pre-click state is only correct if the controlled
|
|
54
|
+
// prop is honoured (a broken impl would fall back to unchecked).
|
|
63
55
|
const [checked, setChecked] = useState(true);
|
|
64
56
|
return (
|
|
65
57
|
<Checkbox.Root
|
|
@@ -71,12 +63,12 @@ describe("Checkbox controlled state", () => {
|
|
|
71
63
|
}
|
|
72
64
|
render(<Harness />);
|
|
73
65
|
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
74
|
-
expect(checkbox).
|
|
66
|
+
expect(checkbox).toBeChecked();
|
|
75
67
|
|
|
76
68
|
// Act & Assert
|
|
77
69
|
await user.click(checkbox);
|
|
78
|
-
expect(checkbox).
|
|
70
|
+
expect(checkbox).not.toBeChecked();
|
|
79
71
|
await user.click(checkbox);
|
|
80
|
-
expect(checkbox).
|
|
72
|
+
expect(checkbox).toBeChecked();
|
|
81
73
|
});
|
|
82
74
|
});
|
|
@@ -1,15 +1,45 @@
|
|
|
1
1
|
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
2
3
|
|
|
3
4
|
import { Checkbox } from "../Checkbox";
|
|
4
5
|
|
|
5
6
|
describe("Checkbox disabled state", () => {
|
|
6
|
-
it(
|
|
7
|
+
it("disables the underlying native input", () => {
|
|
7
8
|
// Arrange & Act
|
|
8
9
|
render(<Checkbox.Root disabled aria-label="Accept terms" />);
|
|
9
|
-
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
10
10
|
|
|
11
11
|
// Assert
|
|
12
|
-
expect(
|
|
12
|
+
expect(screen.getByRole("checkbox", { name: "Accept terms" })).toBeDisabled();
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
it('sets data-disabled="" on the box so CSS can target the disabled state', () => {
|
|
16
|
+
// Arrange & Act
|
|
17
|
+
const { container } = render(<Checkbox.Root disabled aria-label="Accept terms" />);
|
|
18
|
+
|
|
19
|
+
// Assert
|
|
20
|
+
expect(container.querySelector("label")).toHaveAttribute("data-disabled", "");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("omits data-disabled when not disabled", () => {
|
|
24
|
+
// Arrange & Act
|
|
25
|
+
const { container } = render(<Checkbox.Root aria-label="Accept terms" />);
|
|
26
|
+
|
|
27
|
+
// Assert
|
|
28
|
+
expect(container.querySelector("label")).not.toHaveAttribute("data-disabled");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("does not fire onCheckedChange when a disabled checkbox is clicked", async () => {
|
|
32
|
+
// Arrange
|
|
33
|
+
const user = userEvent.setup();
|
|
34
|
+
const onCheckedChange = vi.fn();
|
|
35
|
+
render(
|
|
36
|
+
<Checkbox.Root disabled onCheckedChange={onCheckedChange} aria-label="Accept terms" />,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Act
|
|
40
|
+
await user.click(screen.getByRole("checkbox", { name: "Accept terms" }));
|
|
41
|
+
|
|
42
|
+
// Assert
|
|
43
|
+
expect(onCheckedChange).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
15
45
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Checkbox } from "../Checkbox";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The checkbox is a real native input, so it participates in forms: a checked
|
|
8
|
+
* box submits its `value` under `name`, and an unchecked one submits nothing.
|
|
9
|
+
* The old `<button role="checkbox">` could do neither.
|
|
10
|
+
*/
|
|
11
|
+
describe("Checkbox form participation", () => {
|
|
12
|
+
function NewsletterForm({ onSubmit }: { onSubmit: (value: string | null) => void }) {
|
|
13
|
+
return (
|
|
14
|
+
<form
|
|
15
|
+
onSubmit={(event) => {
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
const data = new FormData(event.currentTarget);
|
|
18
|
+
onSubmit(data.get("newsletter") as string | null);
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<Checkbox.Root name="newsletter" value="weekly" aria-label="Subscribe" />
|
|
22
|
+
<button type="submit">Save</button>
|
|
23
|
+
</form>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
it("submits the value when checked", async () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
const user = userEvent.setup();
|
|
30
|
+
const onSubmit = vi.fn();
|
|
31
|
+
render(<NewsletterForm onSubmit={onSubmit} />);
|
|
32
|
+
|
|
33
|
+
// Act
|
|
34
|
+
await user.click(screen.getByRole("checkbox", { name: "Subscribe" }));
|
|
35
|
+
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
36
|
+
|
|
37
|
+
// Assert
|
|
38
|
+
expect(onSubmit).toHaveBeenCalledWith("weekly");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("submits nothing when unchecked", async () => {
|
|
42
|
+
// Arrange
|
|
43
|
+
const user = userEvent.setup();
|
|
44
|
+
const onSubmit = vi.fn();
|
|
45
|
+
render(<NewsletterForm onSubmit={onSubmit} />);
|
|
46
|
+
|
|
47
|
+
// Act
|
|
48
|
+
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
49
|
+
|
|
50
|
+
// Assert
|
|
51
|
+
expect(onSubmit).toHaveBeenCalledWith(null);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -4,39 +4,34 @@ import userEvent from "@testing-library/user-event";
|
|
|
4
4
|
import { Checkbox } from "../Checkbox";
|
|
5
5
|
|
|
6
6
|
describe("Checkbox indeterminate state", () => {
|
|
7
|
-
it('
|
|
7
|
+
it('marks the input partially-checked (aria mixed) when defaultChecked is "indeterminate"', () => {
|
|
8
8
|
// Arrange & Act
|
|
9
|
-
render(
|
|
10
|
-
<Checkbox.Root
|
|
11
|
-
defaultChecked="indeterminate"
|
|
12
|
-
aria-label="Accept terms"
|
|
13
|
-
/>,
|
|
14
|
-
);
|
|
15
|
-
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
9
|
+
render(<Checkbox.Root defaultChecked="indeterminate" aria-label="Accept terms" />);
|
|
16
10
|
|
|
17
11
|
// Assert
|
|
18
|
-
expect(
|
|
12
|
+
expect(
|
|
13
|
+
screen.getByRole("checkbox", { name: "Accept terms" }),
|
|
14
|
+
).toBePartiallyChecked();
|
|
19
15
|
});
|
|
20
16
|
|
|
21
|
-
it('sets data-state="indeterminate" on the
|
|
17
|
+
it('sets data-state="indeterminate" on the box in indeterminate mode', () => {
|
|
22
18
|
// Arrange & Act
|
|
23
|
-
render(
|
|
24
|
-
<Checkbox.Root
|
|
25
|
-
defaultChecked="indeterminate"
|
|
26
|
-
aria-label="Accept terms"
|
|
27
|
-
/>,
|
|
19
|
+
const { container } = render(
|
|
20
|
+
<Checkbox.Root defaultChecked="indeterminate" aria-label="Accept terms" />,
|
|
28
21
|
);
|
|
29
|
-
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
30
22
|
|
|
31
23
|
// Assert
|
|
32
|
-
expect(
|
|
24
|
+
expect(container.querySelector("label")).toHaveAttribute(
|
|
25
|
+
"data-state",
|
|
26
|
+
"indeterminate",
|
|
27
|
+
);
|
|
33
28
|
});
|
|
34
29
|
|
|
35
30
|
it("resolves to checked=true on the first click (WAI-ARIA tri-state convention)", async () => {
|
|
36
31
|
// Arrange
|
|
37
32
|
const user = userEvent.setup();
|
|
38
33
|
const onCheckedChange = vi.fn();
|
|
39
|
-
render(
|
|
34
|
+
const { container } = render(
|
|
40
35
|
<Checkbox.Root
|
|
41
36
|
defaultChecked="indeterminate"
|
|
42
37
|
onCheckedChange={onCheckedChange}
|
|
@@ -50,33 +45,32 @@ describe("Checkbox indeterminate state", () => {
|
|
|
50
45
|
|
|
51
46
|
// Assert
|
|
52
47
|
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
53
|
-
expect(checkbox).
|
|
54
|
-
expect(checkbox).
|
|
48
|
+
expect(checkbox).toBeChecked();
|
|
49
|
+
expect(checkbox).not.toBePartiallyChecked();
|
|
50
|
+
expect(container.querySelector("label")).toHaveAttribute(
|
|
51
|
+
"data-state",
|
|
52
|
+
"checked",
|
|
53
|
+
);
|
|
55
54
|
});
|
|
56
55
|
|
|
57
56
|
it('honours controlled checked="indeterminate" across re-renders', () => {
|
|
58
57
|
// Arrange
|
|
59
|
-
const { rerender } = render(
|
|
60
|
-
<Checkbox.Root
|
|
61
|
-
checked={false}
|
|
62
|
-
onCheckedChange={() => {}}
|
|
63
|
-
aria-label="Accept terms"
|
|
64
|
-
/>,
|
|
58
|
+
const { container, rerender } = render(
|
|
59
|
+
<Checkbox.Root checked={false} onCheckedChange={() => {}} aria-label="Accept terms" />,
|
|
65
60
|
);
|
|
66
61
|
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
67
|
-
expect(checkbox).
|
|
62
|
+
expect(checkbox).not.toBeChecked();
|
|
68
63
|
|
|
69
64
|
// Act
|
|
70
65
|
rerender(
|
|
71
|
-
<Checkbox.Root
|
|
72
|
-
checked="indeterminate"
|
|
73
|
-
onCheckedChange={() => {}}
|
|
74
|
-
aria-label="Accept terms"
|
|
75
|
-
/>,
|
|
66
|
+
<Checkbox.Root checked="indeterminate" onCheckedChange={() => {}} aria-label="Accept terms" />,
|
|
76
67
|
);
|
|
77
68
|
|
|
78
69
|
// Assert
|
|
79
|
-
expect(checkbox).
|
|
80
|
-
expect(
|
|
70
|
+
expect(checkbox).toBePartiallyChecked();
|
|
71
|
+
expect(container.querySelector("label")).toHaveAttribute(
|
|
72
|
+
"data-state",
|
|
73
|
+
"indeterminate",
|
|
74
|
+
);
|
|
81
75
|
});
|
|
82
76
|
});
|
|
@@ -4,103 +4,80 @@ import userEvent from "@testing-library/user-event";
|
|
|
4
4
|
import { Checkbox } from "../Checkbox";
|
|
5
5
|
|
|
6
6
|
describe("Checkbox.Indicator", () => {
|
|
7
|
-
it("
|
|
8
|
-
// Arrange & Act
|
|
7
|
+
it("is always mounted, regardless of checked state", () => {
|
|
8
|
+
// Arrange & Act — unchecked, yet the mark is in the DOM (CSS hides it).
|
|
9
9
|
render(
|
|
10
10
|
<Checkbox.Root aria-label="Accept terms">
|
|
11
|
-
<Checkbox.Indicator
|
|
12
|
-
</Checkbox.Root>,
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
// Assert
|
|
16
|
-
expect(screen.queryByText("tick")).not.toBeInTheDocument();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("renders its children when the checkbox is checked", () => {
|
|
20
|
-
// Arrange & Act
|
|
21
|
-
render(
|
|
22
|
-
<Checkbox.Root defaultChecked aria-label="Accept terms">
|
|
23
|
-
<Checkbox.Indicator>tick</Checkbox.Indicator>
|
|
11
|
+
<Checkbox.Indicator data-testid="mark" />
|
|
24
12
|
</Checkbox.Root>,
|
|
25
13
|
);
|
|
26
14
|
|
|
27
15
|
// Assert
|
|
28
|
-
expect(screen.
|
|
16
|
+
expect(screen.getByTestId("mark")).toBeInTheDocument();
|
|
29
17
|
});
|
|
30
18
|
|
|
31
|
-
it("
|
|
19
|
+
it("is decorative, carrying aria-hidden", () => {
|
|
32
20
|
// Arrange & Act
|
|
33
21
|
render(
|
|
34
|
-
<Checkbox.Root
|
|
35
|
-
<Checkbox.Indicator
|
|
22
|
+
<Checkbox.Root aria-label="Accept terms">
|
|
23
|
+
<Checkbox.Indicator data-testid="mark" />
|
|
36
24
|
</Checkbox.Root>,
|
|
37
25
|
);
|
|
38
26
|
|
|
39
27
|
// Assert
|
|
40
|
-
expect(screen.
|
|
28
|
+
expect(screen.getByTestId("mark")).toHaveAttribute("aria-hidden", "true");
|
|
41
29
|
});
|
|
42
30
|
|
|
43
|
-
it("
|
|
31
|
+
it("mirrors the checkbox's data-state for CSS to key off", async () => {
|
|
44
32
|
// Arrange
|
|
45
33
|
const user = userEvent.setup();
|
|
46
34
|
render(
|
|
47
35
|
<Checkbox.Root aria-label="Accept terms">
|
|
48
|
-
<Checkbox.Indicator
|
|
36
|
+
<Checkbox.Indicator data-testid="mark" />
|
|
49
37
|
</Checkbox.Root>,
|
|
50
38
|
);
|
|
51
|
-
|
|
39
|
+
const mark = screen.getByTestId("mark");
|
|
40
|
+
expect(mark).toHaveAttribute("data-state", "unchecked");
|
|
52
41
|
|
|
53
42
|
// Act
|
|
54
43
|
await user.click(screen.getByRole("checkbox", { name: "Accept terms" }));
|
|
55
44
|
|
|
56
45
|
// Assert
|
|
57
|
-
expect(
|
|
46
|
+
expect(mark).toHaveAttribute("data-state", "checked");
|
|
58
47
|
});
|
|
59
48
|
|
|
60
|
-
it(
|
|
61
|
-
// Arrange & Act
|
|
62
|
-
render(
|
|
63
|
-
<Checkbox.Root defaultChecked aria-label="Accept terms">
|
|
64
|
-
<Checkbox.Indicator data-testid="indicator">tick</Checkbox.Indicator>
|
|
65
|
-
</Checkbox.Root>,
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
// Assert
|
|
69
|
-
expect(screen.getByTestId("indicator")).toHaveAttribute(
|
|
70
|
-
"aria-hidden",
|
|
71
|
-
"true",
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("mirrors the checkbox's data-state on the indicator", () => {
|
|
49
|
+
it("mirrors the indeterminate data-state", () => {
|
|
76
50
|
// Arrange & Act
|
|
77
51
|
render(
|
|
78
52
|
<Checkbox.Root defaultChecked="indeterminate" aria-label="Accept terms">
|
|
79
|
-
<Checkbox.Indicator data-testid="
|
|
53
|
+
<Checkbox.Indicator data-testid="mark" />
|
|
80
54
|
</Checkbox.Root>,
|
|
81
55
|
);
|
|
82
56
|
|
|
83
57
|
// Assert
|
|
84
|
-
expect(screen.getByTestId("
|
|
58
|
+
expect(screen.getByTestId("mark")).toHaveAttribute(
|
|
85
59
|
"data-state",
|
|
86
60
|
"indeterminate",
|
|
87
61
|
);
|
|
88
62
|
});
|
|
89
63
|
|
|
90
|
-
it("
|
|
64
|
+
it("delegates to the consumer's element (e.g. an svg tick) via asChild", () => {
|
|
91
65
|
// Arrange & Act
|
|
92
66
|
render(
|
|
93
|
-
<Checkbox.Root aria-label="Accept terms">
|
|
94
|
-
<Checkbox.Indicator
|
|
95
|
-
tick
|
|
67
|
+
<Checkbox.Root defaultChecked aria-label="Accept terms">
|
|
68
|
+
<Checkbox.Indicator asChild>
|
|
69
|
+
<svg data-testid="tick" viewBox="0 0 10 10">
|
|
70
|
+
<path d="M1 5l3 3 5-7" />
|
|
71
|
+
</svg>
|
|
96
72
|
</Checkbox.Indicator>
|
|
97
73
|
</Checkbox.Root>,
|
|
98
74
|
);
|
|
99
|
-
const
|
|
75
|
+
const tick = screen.getByTestId("tick");
|
|
100
76
|
|
|
101
77
|
// Assert
|
|
102
|
-
expect(
|
|
103
|
-
expect(
|
|
78
|
+
expect(tick.tagName.toLowerCase()).toBe("svg");
|
|
79
|
+
expect(tick).toHaveAttribute("aria-hidden", "true");
|
|
80
|
+
expect(tick).toHaveAttribute("data-state", "checked");
|
|
104
81
|
});
|
|
105
82
|
|
|
106
83
|
it("throws when rendered outside Checkbox.Root", () => {
|
|
@@ -108,9 +85,7 @@ describe("Checkbox.Indicator", () => {
|
|
|
108
85
|
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
109
86
|
|
|
110
87
|
// Assert
|
|
111
|
-
expect(() =>
|
|
112
|
-
render(<Checkbox.Indicator>tick</Checkbox.Indicator>),
|
|
113
|
-
).toThrow();
|
|
88
|
+
expect(() => render(<Checkbox.Indicator />)).toThrow();
|
|
114
89
|
|
|
115
90
|
error.mockRestore();
|
|
116
91
|
});
|