@primitiv-ui/react 0.1.15 → 0.1.17
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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
|
|
4
|
+
import { Switch } from "../Switch";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The switch is a real native input (a checkbox with the switch role), so it
|
|
8
|
+
* participates in forms: an on switch submits its `value` under `name`, an off
|
|
9
|
+
* one submits nothing. The old `<button role="switch">` could do neither.
|
|
10
|
+
*/
|
|
11
|
+
describe("Switch form participation", () => {
|
|
12
|
+
function SettingsForm({ 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("notify") as string | null);
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<Switch.Root name="notify" value="on" aria-label="Enable notifications">
|
|
22
|
+
<Switch.Thumb />
|
|
23
|
+
</Switch.Root>
|
|
24
|
+
<button type="submit">Save</button>
|
|
25
|
+
</form>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
it("submits the value when on", async () => {
|
|
30
|
+
// Arrange
|
|
31
|
+
const user = userEvent.setup();
|
|
32
|
+
const onSubmit = vi.fn();
|
|
33
|
+
render(<SettingsForm onSubmit={onSubmit} />);
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
await user.click(screen.getByRole("switch", { name: "Enable notifications" }));
|
|
37
|
+
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(onSubmit).toHaveBeenCalledWith("on");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("submits nothing when off", async () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
const user = userEvent.setup();
|
|
46
|
+
const onSubmit = vi.fn();
|
|
47
|
+
render(<SettingsForm onSubmit={onSubmit} />);
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
expect(onSubmit).toHaveBeenCalledWith(null);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -15,10 +15,10 @@ describe("Switch keyboard interaction", () => {
|
|
|
15
15
|
await user.keyboard(" ");
|
|
16
16
|
|
|
17
17
|
// Assert
|
|
18
|
-
expect(sw).
|
|
18
|
+
expect(sw).toBeChecked();
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
it("
|
|
21
|
+
it("does not toggle on Enter (native checkbox semantics)", async () => {
|
|
22
22
|
// Arrange
|
|
23
23
|
const user = userEvent.setup();
|
|
24
24
|
render(<Switch.Root aria-label="Enable notifications" />);
|
|
@@ -29,7 +29,7 @@ describe("Switch keyboard interaction", () => {
|
|
|
29
29
|
await user.keyboard("{Enter}");
|
|
30
30
|
|
|
31
31
|
// Assert
|
|
32
|
-
expect(sw).
|
|
32
|
+
expect(sw).not.toBeChecked();
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
it("does not toggle on Space when disabled", async () => {
|
|
@@ -51,6 +51,6 @@ describe("Switch keyboard interaction", () => {
|
|
|
51
51
|
|
|
52
52
|
// Assert
|
|
53
53
|
expect(onCheckedChange).not.toHaveBeenCalled();
|
|
54
|
-
expect(sw).
|
|
54
|
+
expect(sw).not.toBeChecked();
|
|
55
55
|
});
|
|
56
56
|
});
|
|
@@ -11,41 +11,40 @@ describe("Switch uncontrolled state", () => {
|
|
|
11
11
|
// Assert
|
|
12
12
|
expect(
|
|
13
13
|
screen.getByRole("switch", { name: "Enable notifications" }),
|
|
14
|
-
).
|
|
14
|
+
).not.toBeChecked();
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it("starts checked when defaultChecked={true}", () => {
|
|
18
18
|
// Arrange & Act
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
expect(
|
|
23
|
-
screen.getByRole("switch", { name: "Enable notifications" }),
|
|
24
|
-
).toHaveAttribute("aria-checked", "true");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('sets data-state="checked" when defaultChecked={true}', () => {
|
|
28
|
-
// Arrange & Act
|
|
29
|
-
render(<Switch.Root aria-label="Enable notifications" defaultChecked />);
|
|
19
|
+
const { container } = render(
|
|
20
|
+
<Switch.Root aria-label="Enable notifications" defaultChecked />,
|
|
21
|
+
);
|
|
30
22
|
|
|
31
23
|
// Assert
|
|
32
24
|
expect(
|
|
33
25
|
screen.getByRole("switch", { name: "Enable notifications" }),
|
|
34
|
-
).
|
|
26
|
+
).toBeChecked();
|
|
27
|
+
expect(container.querySelector("label")).toHaveAttribute(
|
|
28
|
+
"data-state",
|
|
29
|
+
"checked",
|
|
30
|
+
);
|
|
35
31
|
});
|
|
36
32
|
|
|
37
33
|
it("toggles to checked on first click", async () => {
|
|
38
34
|
// Arrange
|
|
39
35
|
const user = userEvent.setup();
|
|
40
|
-
render(<Switch.Root aria-label="Enable notifications" />);
|
|
36
|
+
const { container } = render(<Switch.Root aria-label="Enable notifications" />);
|
|
41
37
|
const sw = screen.getByRole("switch", { name: "Enable notifications" });
|
|
42
38
|
|
|
43
39
|
// Act
|
|
44
40
|
await user.click(sw);
|
|
45
41
|
|
|
46
42
|
// Assert
|
|
47
|
-
expect(sw).
|
|
48
|
-
expect(
|
|
43
|
+
expect(sw).toBeChecked();
|
|
44
|
+
expect(container.querySelector("label")).toHaveAttribute(
|
|
45
|
+
"data-state",
|
|
46
|
+
"checked",
|
|
47
|
+
);
|
|
49
48
|
});
|
|
50
49
|
|
|
51
50
|
it("toggles back to unchecked on second click", async () => {
|
|
@@ -58,17 +57,54 @@ describe("Switch uncontrolled state", () => {
|
|
|
58
57
|
await user.click(sw);
|
|
59
58
|
|
|
60
59
|
// Assert
|
|
61
|
-
expect(sw).
|
|
62
|
-
expect(sw).toHaveAttribute("data-state", "unchecked");
|
|
60
|
+
expect(sw).not.toBeChecked();
|
|
63
61
|
});
|
|
64
62
|
|
|
65
63
|
it("fires onCheckedChange with the new value on each toggle", async () => {
|
|
64
|
+
// Arrange
|
|
65
|
+
const user = userEvent.setup();
|
|
66
|
+
const onCheckedChange = vi.fn();
|
|
67
|
+
render(
|
|
68
|
+
<Switch.Root aria-label="Enable notifications" onCheckedChange={onCheckedChange} />,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Act
|
|
72
|
+
await user.click(screen.getByRole("switch", { name: "Enable notifications" }));
|
|
73
|
+
|
|
74
|
+
// Assert
|
|
75
|
+
expect(onCheckedChange).toHaveBeenCalledOnce();
|
|
76
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("composes a consumer onChange, running it before the internal handler", async () => {
|
|
80
|
+
// Arrange
|
|
81
|
+
const user = userEvent.setup();
|
|
82
|
+
const order: string[] = [];
|
|
83
|
+
const onChange = vi.fn(() => order.push("consumer"));
|
|
84
|
+
const onCheckedChange = vi.fn(() => order.push("internal"));
|
|
85
|
+
render(
|
|
86
|
+
<Switch.Root
|
|
87
|
+
aria-label="Enable notifications"
|
|
88
|
+
onChange={onChange}
|
|
89
|
+
onCheckedChange={onCheckedChange}
|
|
90
|
+
/>,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Act
|
|
94
|
+
await user.click(screen.getByRole("switch", { name: "Enable notifications" }));
|
|
95
|
+
|
|
96
|
+
// Assert
|
|
97
|
+
expect(order).toEqual(["consumer", "internal"]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("lets a consumer onChange veto the internal handler via preventDefault", async () => {
|
|
66
101
|
// Arrange
|
|
67
102
|
const user = userEvent.setup();
|
|
68
103
|
const onCheckedChange = vi.fn();
|
|
69
104
|
render(
|
|
70
105
|
<Switch.Root
|
|
71
106
|
aria-label="Enable notifications"
|
|
107
|
+
onChange={(event) => event.preventDefault()}
|
|
72
108
|
onCheckedChange={onCheckedChange}
|
|
73
109
|
/>,
|
|
74
110
|
);
|
|
@@ -77,7 +113,6 @@ describe("Switch uncontrolled state", () => {
|
|
|
77
113
|
await user.click(screen.getByRole("switch", { name: "Enable notifications" }));
|
|
78
114
|
|
|
79
115
|
// Assert
|
|
80
|
-
expect(onCheckedChange).
|
|
81
|
-
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
116
|
+
expect(onCheckedChange).not.toHaveBeenCalled();
|
|
82
117
|
});
|
|
83
118
|
});
|
|
@@ -1,28 +1,46 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { ChangeEvent } from "react";
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import { SwitchChangeHandler } from "../types";
|
|
4
5
|
|
|
5
6
|
type UseSwitchRootArgs = {
|
|
6
7
|
defaultChecked?: boolean;
|
|
7
8
|
checked?: boolean;
|
|
8
9
|
onCheckedChange?: (checked: boolean) => void;
|
|
10
|
+
onChange?: SwitchChangeHandler;
|
|
9
11
|
};
|
|
10
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Resolves the controlled / uncontrolled state for a {@link Switch.Root} and
|
|
15
|
+
* produces the props to spread onto its native `<input type="checkbox"
|
|
16
|
+
* role="switch">`.
|
|
17
|
+
*
|
|
18
|
+
* In uncontrolled mode the input is rendered with `defaultChecked` and no
|
|
19
|
+
* `checked` prop, so the browser owns the value and the switch participates in
|
|
20
|
+
* forms and resets. The returned `checked` is a best-effort mirror for the
|
|
21
|
+
* `data-state` hook, updated from the input's own `change` events.
|
|
22
|
+
*/
|
|
11
23
|
export function useSwitchRoot({
|
|
12
24
|
defaultChecked,
|
|
13
25
|
checked: controlledChecked,
|
|
14
26
|
onCheckedChange,
|
|
27
|
+
onChange,
|
|
15
28
|
}: UseSwitchRootArgs) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
);
|
|
29
|
+
const isControlled = controlledChecked !== undefined;
|
|
30
|
+
const [mirror, setMirror] = useState(defaultChecked ?? false);
|
|
31
|
+
const checked = isControlled ? controlledChecked : mirror;
|
|
20
32
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
34
|
+
onChange?.(event);
|
|
35
|
+
if (event.defaultPrevented) return;
|
|
36
|
+
const next = event.target.checked;
|
|
37
|
+
if (!isControlled) setMirror(next);
|
|
24
38
|
onCheckedChange?.(next);
|
|
25
|
-
}
|
|
39
|
+
};
|
|
26
40
|
|
|
27
|
-
|
|
41
|
+
const inputStateProps = isControlled
|
|
42
|
+
? { checked: controlledChecked }
|
|
43
|
+
: { defaultChecked: defaultChecked ?? false };
|
|
44
|
+
|
|
45
|
+
return { checked, handleChange, inputStateProps };
|
|
28
46
|
}
|
package/src/Switch/types.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ChangeEventHandler, ComponentProps, ReactNode, Ref } from "react";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Props common to both controlled and uncontrolled `Switch.Root` modes — the
|
|
5
|
+
* native `<input type="checkbox">` attributes (minus the ones the component
|
|
6
|
+
* owns). `className` / `style` style the **track** (the visible control), not
|
|
7
|
+
* the hidden input; everything else spreads onto the input, because
|
|
8
|
+
* semantically the Root *is* the switch.
|
|
9
|
+
*/
|
|
4
10
|
export type SwitchRootBaseProps = Omit<
|
|
5
|
-
|
|
6
|
-
"type" | "role" | "
|
|
11
|
+
ComponentProps<"input">,
|
|
12
|
+
"type" | "role" | "checked" | "defaultChecked"
|
|
7
13
|
> & {
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
/** Ref to the
|
|
11
|
-
ref?: Ref<
|
|
14
|
+
/** Optional content (typically `Switch.Thumb`). */
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
/** Ref to the underlying native `<input>` element. */
|
|
17
|
+
ref?: Ref<HTMLInputElement>;
|
|
12
18
|
};
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
|
-
* Props for `Switch.Root` in uncontrolled mode — the
|
|
16
|
-
* state. Pass `defaultChecked` to set the initial value; `checked` is
|
|
21
|
+
* Props for `Switch.Root` in uncontrolled mode — the **browser** owns the
|
|
22
|
+
* checked state. Pass `defaultChecked` to set the initial value; `checked` is
|
|
23
|
+
* forbidden.
|
|
17
24
|
*/
|
|
18
25
|
export type SwitchRootUncontrolledProps = SwitchRootBaseProps & {
|
|
19
26
|
/** Initial checked state when uncontrolled. */
|
|
@@ -56,3 +63,6 @@ export type SwitchThumbProps = ComponentProps<"span"> & {
|
|
|
56
63
|
/** Render the child element instead of the default `<span>`. */
|
|
57
64
|
asChild?: boolean;
|
|
58
65
|
};
|
|
66
|
+
|
|
67
|
+
/** Internal: the native `onChange` shape the Root composes with its own. */
|
|
68
|
+
export type SwitchChangeHandler = ChangeEventHandler<HTMLInputElement>;
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
|
|
4
|
-
import { Checkbox } from "../Checkbox";
|
|
5
|
-
|
|
6
|
-
describe("Checkbox asChild composition", () => {
|
|
7
|
-
it("Root asChild delegates to the child element while keeping ARIA and toggle wiring", async () => {
|
|
8
|
-
// Arrange
|
|
9
|
-
const user = userEvent.setup();
|
|
10
|
-
const onCheckedChange = vi.fn();
|
|
11
|
-
render(
|
|
12
|
-
<Checkbox.Root
|
|
13
|
-
asChild
|
|
14
|
-
onCheckedChange={onCheckedChange}
|
|
15
|
-
aria-label="Accept terms"
|
|
16
|
-
>
|
|
17
|
-
<li>Accept</li>
|
|
18
|
-
</Checkbox.Root>,
|
|
19
|
-
);
|
|
20
|
-
const checkbox = screen.getByRole("checkbox", { name: "Accept terms" });
|
|
21
|
-
|
|
22
|
-
// Assert element is the consumer's <li>
|
|
23
|
-
expect(checkbox.tagName).toBe("LI");
|
|
24
|
-
// ARIA + data-state merged onto the child
|
|
25
|
-
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
|
26
|
-
expect(checkbox).toHaveAttribute("data-state", "unchecked");
|
|
27
|
-
|
|
28
|
-
// Act
|
|
29
|
-
await user.click(checkbox);
|
|
30
|
-
|
|
31
|
-
// Assert toggle still fires through composed onClick
|
|
32
|
-
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
33
|
-
expect(checkbox).toHaveAttribute("aria-checked", "true");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("Root asChild lets the consumer override the role for menu-item contexts", () => {
|
|
37
|
-
// Arrange & Act
|
|
38
|
-
render(
|
|
39
|
-
<Checkbox.Root asChild aria-label="Accept terms">
|
|
40
|
-
<li role="menuitemcheckbox">Accept</li>
|
|
41
|
-
</Checkbox.Root>,
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
// Assert
|
|
45
|
-
const item = screen.getByRole("menuitemcheckbox", { name: "Accept terms" });
|
|
46
|
-
expect(item).toHaveAttribute("aria-checked", "false");
|
|
47
|
-
expect(item).toHaveAttribute("data-state", "unchecked");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("Indicator asChild delegates rendering to the consumer's element", () => {
|
|
51
|
-
// Arrange & Act
|
|
52
|
-
render(
|
|
53
|
-
<Checkbox.Root defaultChecked aria-label="Accept terms">
|
|
54
|
-
<Checkbox.Indicator asChild>
|
|
55
|
-
<svg data-testid="tick" viewBox="0 0 10 10">
|
|
56
|
-
<path d="M1 5l3 3 5-7" />
|
|
57
|
-
</svg>
|
|
58
|
-
</Checkbox.Indicator>
|
|
59
|
-
</Checkbox.Root>,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
// Assert
|
|
63
|
-
const tick = screen.getByTestId("tick");
|
|
64
|
-
expect(tick.tagName.toLowerCase()).toBe("svg");
|
|
65
|
-
expect(tick).toHaveAttribute("aria-hidden", "true");
|
|
66
|
-
expect(tick).toHaveAttribute("data-state", "checked");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
|
|
4
|
-
import { Radio } from "../Radio";
|
|
5
|
-
|
|
6
|
-
describe("Radio asChild composition", () => {
|
|
7
|
-
it("Root asChild delegates to the child element while keeping ARIA and select wiring", async () => {
|
|
8
|
-
// Arrange
|
|
9
|
-
const user = userEvent.setup();
|
|
10
|
-
const onCheckedChange = vi.fn();
|
|
11
|
-
render(
|
|
12
|
-
<Radio.Root asChild onCheckedChange={onCheckedChange} aria-label="Compact">
|
|
13
|
-
<li>Compact</li>
|
|
14
|
-
</Radio.Root>,
|
|
15
|
-
);
|
|
16
|
-
const radio = screen.getByRole("radio", { name: "Compact" });
|
|
17
|
-
|
|
18
|
-
// Assert element is the consumer's <li>
|
|
19
|
-
expect(radio.tagName).toBe("LI");
|
|
20
|
-
expect(radio).toHaveAttribute("aria-checked", "false");
|
|
21
|
-
expect(radio).toHaveAttribute("data-state", "unchecked");
|
|
22
|
-
|
|
23
|
-
// Act
|
|
24
|
-
await user.click(radio);
|
|
25
|
-
|
|
26
|
-
// Assert select still fires through composed onClick
|
|
27
|
-
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
28
|
-
expect(radio).toHaveAttribute("aria-checked", "true");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("Root asChild lets the consumer override the role for menu-item contexts", () => {
|
|
32
|
-
// Arrange & Act
|
|
33
|
-
render(
|
|
34
|
-
<Radio.Root asChild aria-label="Compact">
|
|
35
|
-
<li role="menuitemradio">Compact</li>
|
|
36
|
-
</Radio.Root>,
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
// Assert
|
|
40
|
-
const item = screen.getByRole("menuitemradio", { name: "Compact" });
|
|
41
|
-
expect(item).toHaveAttribute("aria-checked", "false");
|
|
42
|
-
expect(item).toHaveAttribute("data-state", "unchecked");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("Indicator asChild delegates rendering to the consumer's element", () => {
|
|
46
|
-
// Arrange & Act
|
|
47
|
-
render(
|
|
48
|
-
<Radio.Root defaultChecked aria-label="Compact">
|
|
49
|
-
<Radio.Indicator asChild>
|
|
50
|
-
<svg data-testid="dot" viewBox="0 0 10 10">
|
|
51
|
-
<circle cx="5" cy="5" r="3" />
|
|
52
|
-
</svg>
|
|
53
|
-
</Radio.Indicator>
|
|
54
|
-
</Radio.Root>,
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
// Assert
|
|
58
|
-
const dot = screen.getByTestId("dot");
|
|
59
|
-
expect(dot.tagName.toLowerCase()).toBe("svg");
|
|
60
|
-
expect(dot).toHaveAttribute("aria-hidden", "true");
|
|
61
|
-
expect(dot).toHaveAttribute("data-state", "checked");
|
|
62
|
-
});
|
|
63
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
|
|
4
|
-
import { Radio } from "../Radio";
|
|
5
|
-
|
|
6
|
-
describe("Radio.Indicator", () => {
|
|
7
|
-
it("does not render its children when the radio is unchecked", () => {
|
|
8
|
-
// Arrange & Act
|
|
9
|
-
render(
|
|
10
|
-
<Radio.Root aria-label="Compact">
|
|
11
|
-
<Radio.Indicator>dot</Radio.Indicator>
|
|
12
|
-
</Radio.Root>,
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
// Assert
|
|
16
|
-
expect(screen.queryByText("dot")).not.toBeInTheDocument();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("renders its children when the radio is checked", () => {
|
|
20
|
-
// Arrange & Act
|
|
21
|
-
render(
|
|
22
|
-
<Radio.Root defaultChecked aria-label="Compact">
|
|
23
|
-
<Radio.Indicator>dot</Radio.Indicator>
|
|
24
|
-
</Radio.Root>,
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
// Assert
|
|
28
|
-
expect(screen.getByText("dot")).toBeInTheDocument();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("mounts in response to selecting", async () => {
|
|
32
|
-
// Arrange
|
|
33
|
-
const user = userEvent.setup();
|
|
34
|
-
render(
|
|
35
|
-
<Radio.Root aria-label="Compact">
|
|
36
|
-
<Radio.Indicator>dot</Radio.Indicator>
|
|
37
|
-
</Radio.Root>,
|
|
38
|
-
);
|
|
39
|
-
expect(screen.queryByText("dot")).not.toBeInTheDocument();
|
|
40
|
-
|
|
41
|
-
// Act
|
|
42
|
-
await user.click(screen.getByRole("radio", { name: "Compact" }));
|
|
43
|
-
|
|
44
|
-
// Assert
|
|
45
|
-
expect(screen.getByText("dot")).toBeInTheDocument();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('carries aria-hidden="true" since it is decorative', () => {
|
|
49
|
-
// Arrange & Act
|
|
50
|
-
render(
|
|
51
|
-
<Radio.Root defaultChecked aria-label="Compact">
|
|
52
|
-
<Radio.Indicator data-testid="indicator">dot</Radio.Indicator>
|
|
53
|
-
</Radio.Root>,
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
// Assert
|
|
57
|
-
expect(screen.getByTestId("indicator")).toHaveAttribute(
|
|
58
|
-
"aria-hidden",
|
|
59
|
-
"true",
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("mirrors the radio's data-state on the indicator", () => {
|
|
64
|
-
// Arrange & Act
|
|
65
|
-
render(
|
|
66
|
-
<Radio.Root defaultChecked aria-label="Compact">
|
|
67
|
-
<Radio.Indicator data-testid="indicator">dot</Radio.Indicator>
|
|
68
|
-
</Radio.Root>,
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
// Assert
|
|
72
|
-
expect(screen.getByTestId("indicator")).toHaveAttribute(
|
|
73
|
-
"data-state",
|
|
74
|
-
"checked",
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("stays in the DOM while unchecked when forceMount is set", () => {
|
|
79
|
-
// Arrange & Act
|
|
80
|
-
render(
|
|
81
|
-
<Radio.Root aria-label="Compact">
|
|
82
|
-
<Radio.Indicator forceMount data-testid="indicator">
|
|
83
|
-
dot
|
|
84
|
-
</Radio.Indicator>
|
|
85
|
-
</Radio.Root>,
|
|
86
|
-
);
|
|
87
|
-
const indicator = screen.getByTestId("indicator");
|
|
88
|
-
|
|
89
|
-
// Assert
|
|
90
|
-
expect(indicator).toBeInTheDocument();
|
|
91
|
-
expect(indicator).toHaveAttribute("data-state", "unchecked");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("throws when rendered outside Radio.Root", () => {
|
|
95
|
-
// Arrange
|
|
96
|
-
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
97
|
-
|
|
98
|
-
// Assert
|
|
99
|
-
expect(() => render(<Radio.Indicator>dot</Radio.Indicator>)).toThrow();
|
|
100
|
-
|
|
101
|
-
error.mockRestore();
|
|
102
|
-
});
|
|
103
|
-
});
|