@primitiv-ui/react 0.1.15 → 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.
Files changed (44) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/src/Checkbox/Checkbox.tsx +96 -120
  4. package/src/Checkbox/CheckboxContext.ts +8 -0
  5. package/src/Checkbox/README.md +69 -88
  6. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +48 -10
  7. package/src/Checkbox/__tests__/Checkbox.contract.test.tsx +29 -40
  8. package/src/Checkbox/__tests__/Checkbox.controlled-state.test.tsx +16 -24
  9. package/src/Checkbox/__tests__/Checkbox.disabled.test.tsx +33 -3
  10. package/src/Checkbox/__tests__/Checkbox.form.test.tsx +53 -0
  11. package/src/Checkbox/__tests__/Checkbox.indeterminate.test.tsx +28 -34
  12. package/src/Checkbox/__tests__/Checkbox.indicator.test.tsx +27 -52
  13. package/src/Checkbox/__tests__/Checkbox.uncontrolled-state.test.tsx +33 -17
  14. package/src/Checkbox/hooks/index.ts +1 -0
  15. package/src/Checkbox/hooks/useCheckboxInput.ts +49 -0
  16. package/src/Checkbox/types.ts +24 -17
  17. package/src/Radio/README.md +89 -86
  18. package/src/Radio/Radio.tsx +87 -98
  19. package/src/Radio/RadioContext.ts +9 -0
  20. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +34 -9
  21. package/src/Radio/__tests__/Radio.contract.test.tsx +30 -36
  22. package/src/Radio/__tests__/Radio.controlled-state.test.tsx +21 -11
  23. package/src/Radio/__tests__/Radio.disabled.test.tsx +31 -6
  24. package/src/Radio/__tests__/Radio.native-grouping.test.tsx +77 -0
  25. package/src/Radio/__tests__/Radio.parts.test.tsx +76 -0
  26. package/src/Radio/__tests__/Radio.uncontrolled-state.test.tsx +39 -32
  27. package/src/Radio/hooks/useRadioRoot.ts +32 -16
  28. package/src/Radio/types.ts +30 -16
  29. package/src/Switch/README.md +45 -57
  30. package/src/Switch/Switch.tsx +78 -65
  31. package/src/Switch/SwitchContext.ts +3 -1
  32. package/src/Switch/__tests__/Switch.asChild.test.tsx +16 -34
  33. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +47 -28
  34. package/src/Switch/__tests__/Switch.contract.test.tsx +21 -16
  35. package/src/Switch/__tests__/Switch.controlled-state.test.tsx +10 -12
  36. package/src/Switch/__tests__/Switch.disabled.test.tsx +12 -17
  37. package/src/Switch/__tests__/Switch.form.test.tsx +55 -0
  38. package/src/Switch/__tests__/Switch.keyboard-interaction.test.tsx +4 -4
  39. package/src/Switch/__tests__/Switch.uncontrolled-state.test.tsx +55 -20
  40. package/src/Switch/hooks/useSwitchRoot.ts +29 -11
  41. package/src/Switch/types.ts +20 -10
  42. package/src/Checkbox/__tests__/Checkbox.asChild.test.tsx +0 -69
  43. package/src/Radio/__tests__/Radio.asChild.test.tsx +0 -63
  44. 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).toHaveAttribute("aria-checked", "true");
18
+ expect(sw).toBeChecked();
19
19
  });
20
20
 
21
- it("toggles on Enter key", async () => {
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).toHaveAttribute("aria-checked", "true");
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).toHaveAttribute("aria-checked", "false");
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
- ).toHaveAttribute("aria-checked", "false");
14
+ ).not.toBeChecked();
15
15
  });
16
16
 
17
17
  it("starts checked when defaultChecked={true}", () => {
18
18
  // Arrange & Act
19
- render(<Switch.Root aria-label="Enable notifications" defaultChecked />);
20
-
21
- // Assert
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
- ).toHaveAttribute("data-state", "checked");
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).toHaveAttribute("aria-checked", "true");
48
- expect(sw).toHaveAttribute("data-state", "checked");
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).toHaveAttribute("aria-checked", "false");
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).toHaveBeenCalledOnce();
81
- expect(onCheckedChange).toHaveBeenCalledWith(true);
116
+ expect(onCheckedChange).not.toHaveBeenCalled();
82
117
  });
83
118
  });
@@ -1,28 +1,46 @@
1
- import { useCallback } from "react";
1
+ import { useState } from "react";
2
+ import type { ChangeEvent } from "react";
2
3
 
3
- import { useControllableState } from "../../hooks/index.ts";
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 [checked, setChecked] = useControllableState<boolean>(
17
- controlledChecked,
18
- defaultChecked ?? false,
19
- );
29
+ const isControlled = controlledChecked !== undefined;
30
+ const [mirror, setMirror] = useState(defaultChecked ?? false);
31
+ const checked = isControlled ? controlledChecked : mirror;
20
32
 
21
- const toggle = useCallback(() => {
22
- const next = !checked;
23
- setChecked(next);
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
- }, [checked, setChecked, onCheckedChange]);
39
+ };
26
40
 
27
- return { checked, toggle };
41
+ const inputStateProps = isControlled
42
+ ? { checked: controlledChecked }
43
+ : { defaultChecked: defaultChecked ?? false };
44
+
45
+ return { checked, handleChange, inputStateProps };
28
46
  }
@@ -1,19 +1,26 @@
1
- import { ButtonHTMLAttributes, ComponentProps, ReactNode, Ref } from "react";
1
+ import { ChangeEventHandler, ComponentProps, ReactNode, Ref } from "react";
2
2
 
3
- /** Props common to both controlled and uncontrolled `Switch.Root` modes. */
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
- ButtonHTMLAttributes<HTMLButtonElement>,
6
- "type" | "role" | "aria-checked"
11
+ ComponentProps<"input">,
12
+ "type" | "role" | "checked" | "defaultChecked"
7
13
  > & {
8
- /** Render the child element instead of the default `<button>`. */
9
- asChild?: boolean;
10
- /** Ref to the rendered `<button>` element. */
11
- ref?: Ref<HTMLButtonElement>;
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 component owns the checked
16
- * state. Pass `defaultChecked` to set the initial value; `checked` is forbidden.
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
- });