@bug-on/md3-react 2.0.3 → 3.0.1
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/.turbo/turbo-build.log +42 -0
- package/CHANGELOG.md +69 -0
- package/dist/index.css +178 -0
- package/dist/index.css.d.ts +2 -0
- package/dist/index.d.mts +6135 -0
- package/dist/index.d.ts +6135 -71
- package/dist/index.js +1688 -631
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1600 -564
- package/dist/index.mjs.map +1 -1
- package/dist/material-symbols-cdn.css.d.ts +2 -0
- package/dist/material-symbols-self-hosted.css.d.ts +2 -0
- package/dist/plugin.d.mts +1 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +13 -0
- package/dist/plugin.js.map +1 -0
- package/dist/plugin.mjs +3 -0
- package/dist/plugin.mjs.map +1 -0
- package/dist/typography.css.d.ts +2 -0
- package/package.json +28 -19
- package/scripts/copy-assets.js +115 -0
- package/src/assets/fonts/GoogleSansFlex-VariableFont.woff2 +0 -0
- package/src/assets/fonts/MaterialSymbolsOutlined-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/fonts/MaterialSymbolsRounded-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/fonts/MaterialSymbolsSharp-VariableFont_FILL,GRAD,opsz,wght.ttf +0 -0
- package/src/assets/loading-indicator.svg +19 -0
- package/src/assets/material-symbols-cdn.css +65 -0
- package/src/assets/material-symbols-self-hosted.css +90 -0
- package/src/css.d.ts +20 -0
- package/src/hooks/useClickOutside.ts +37 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/useRipple.ts +88 -0
- package/src/index.css +23 -0
- package/src/index.ts +349 -0
- package/src/lib/material-symbols-preconnect.tsx +82 -0
- package/src/lib/theme-utils.ts +195 -0
- package/src/lib/utils.ts +6 -0
- package/src/plugin.ts +12 -0
- package/src/test/button.test.tsx +59 -0
- package/src/test/icon.test.tsx +91 -0
- package/src/test/loading-indicator.test.tsx +128 -0
- package/src/test/progress-indicator.test.tsx +306 -0
- package/src/test/setup.ts +80 -0
- package/src/test/typography.test.tsx +206 -0
- package/src/types/index.ts +7 -0
- package/src/types/md3.ts +31 -0
- package/src/ui/Text.tsx +60 -0
- package/src/ui/__snapshots__/divider.test.tsx.snap +63 -0
- package/src/ui/app-bar/app-bar-column.tsx +99 -0
- package/src/ui/app-bar/app-bar-item-button.tsx +71 -0
- package/src/ui/app-bar/app-bar-items.test.tsx +89 -0
- package/src/ui/app-bar/app-bar-overflow-indicator.tsx +108 -0
- package/src/ui/app-bar/app-bar-row.tsx +104 -0
- package/src/ui/app-bar/app-bar.test.tsx +87 -0
- package/src/ui/app-bar/app-bar.tokens.ts +223 -0
- package/src/ui/app-bar/app-bar.types.ts +441 -0
- package/src/ui/app-bar/bottom-app-bar.test.tsx +42 -0
- package/src/ui/app-bar/bottom-app-bar.tsx +84 -0
- package/src/ui/app-bar/docked-toolbar.test.tsx +34 -0
- package/src/ui/app-bar/docked-toolbar.tsx +54 -0
- package/src/ui/app-bar/flexible-app-bar.test.tsx +75 -0
- package/src/ui/app-bar/hooks/use-app-bar-scroll.ts +110 -0
- package/src/ui/app-bar/hooks/use-flexible-app-bar.ts +123 -0
- package/{dist/ui/app-bar/index.d.ts → src/ui/app-bar/index.ts} +35 -2
- package/src/ui/app-bar/large-flexible-app-bar.tsx +165 -0
- package/src/ui/app-bar/medium-flexible-app-bar.tsx +167 -0
- package/src/ui/app-bar/search-app-bar.test.tsx +49 -0
- package/src/ui/app-bar/search-app-bar.tsx +176 -0
- package/src/ui/app-bar/search-view.tsx +227 -0
- package/src/ui/app-bar/small-app-bar.test.tsx +48 -0
- package/src/ui/app-bar/small-app-bar.tsx +203 -0
- package/src/ui/badge.test.tsx +345 -0
- package/src/ui/badge.tsx +282 -0
- package/src/ui/button-group.test.tsx +71 -0
- package/src/ui/button-group.tsx +350 -0
- package/src/ui/button.test.tsx +306 -0
- package/src/ui/button.tsx +665 -0
- package/src/ui/card.test.tsx +187 -0
- package/src/ui/card.tsx +259 -0
- package/src/ui/checkbox.test.tsx +423 -0
- package/src/ui/checkbox.tsx +525 -0
- package/src/ui/chip.test.tsx +292 -0
- package/src/ui/chip.tsx +548 -0
- package/src/ui/code-block.tsx +219 -0
- package/src/ui/dialog.test.tsx +300 -0
- package/src/ui/dialog.tsx +384 -0
- package/src/ui/divider.test.tsx +314 -0
- package/src/ui/divider.tsx +412 -0
- package/src/ui/drawer.tsx +240 -0
- package/src/ui/fab-menu.test.tsx +494 -0
- package/src/ui/fab-menu.tsx +739 -0
- package/src/ui/fab.test.tsx +232 -0
- package/src/ui/fab.tsx +505 -0
- package/src/ui/icon-button.test.tsx +515 -0
- package/src/ui/icon-button.tsx +525 -0
- package/src/ui/icon.test.tsx +197 -0
- package/src/ui/icon.tsx +179 -0
- package/src/ui/loading-indicator.test.tsx +73 -0
- package/src/ui/loading-indicator.tsx +312 -0
- package/src/ui/menu/context-menu.tsx +275 -0
- package/src/ui/menu/index.ts +77 -0
- package/src/ui/menu/menu-animations.ts +102 -0
- package/src/ui/menu/menu-context.tsx +99 -0
- package/src/ui/menu/menu-divider.tsx +47 -0
- package/src/ui/menu/menu-group.tsx +200 -0
- package/src/ui/menu/menu-item.tsx +294 -0
- package/src/ui/menu/menu-tokens.ts +208 -0
- package/src/ui/menu/menu-types.ts +313 -0
- package/src/ui/menu/menu.test.tsx +624 -0
- package/src/ui/menu/menu.tsx +289 -0
- package/src/ui/menu/sub-menu.tsx +223 -0
- package/src/ui/menu/vertical-menu.tsx +382 -0
- package/src/ui/navigation-rail.test.tsx +404 -0
- package/src/ui/navigation-rail.tsx +607 -0
- package/src/ui/progress-indicator/circular.tsx +248 -0
- package/src/ui/progress-indicator/hooks.ts +51 -0
- package/{dist/ui/progress-indicator/index.d.ts → src/ui/progress-indicator/index.tsx} +20 -2
- package/src/ui/progress-indicator/linear-flat.tsx +83 -0
- package/src/ui/progress-indicator/linear-wavy.tsx +243 -0
- package/src/ui/progress-indicator/linear.tsx +143 -0
- package/src/ui/progress-indicator/types.ts +158 -0
- package/src/ui/progress-indicator/utils.ts +73 -0
- package/src/ui/radio-button.test.tsx +407 -0
- package/src/ui/radio-button.tsx +551 -0
- package/src/ui/ripple.test.tsx +72 -0
- package/src/ui/ripple.tsx +234 -0
- package/src/ui/scroll-area.test.tsx +58 -0
- package/src/ui/scroll-area.tsx +139 -0
- package/src/ui/search/animated-placeholder.tsx +145 -0
- package/src/ui/search/hooks/use-search-keyboard.test.ts +202 -0
- package/src/ui/search/hooks/use-search-keyboard.ts +104 -0
- package/src/ui/search/hooks/use-search-view-focus.test.ts +96 -0
- package/src/ui/search/hooks/use-search-view-focus.ts +24 -0
- package/src/ui/search/index.ts +44 -0
- package/src/ui/search/search-bar.tsx +220 -0
- package/src/ui/search/search-context.tsx +42 -0
- package/src/ui/search/search-view-docked.tsx +194 -0
- package/src/ui/search/search-view-fullscreen.tsx +247 -0
- package/src/ui/search/search.test.tsx +233 -0
- package/src/ui/search/search.tokens.ts +134 -0
- package/src/ui/search/search.tsx +131 -0
- package/src/ui/search/search.types.ts +154 -0
- package/src/ui/search/trailing-action.tsx +49 -0
- package/src/ui/shared/constants.ts +135 -0
- package/{dist/ui/shared/touch-target.d.ts → src/ui/shared/touch-target.tsx} +13 -1
- package/src/ui/slider/hooks/useSliderMath.ts +195 -0
- package/{dist/ui/slider/index.d.ts → src/ui/slider/index.ts} +12 -1
- package/src/ui/slider/range-slider.tsx +561 -0
- package/src/ui/slider/slider-thumb.tsx +379 -0
- package/src/ui/slider/slider-track.tsx +912 -0
- package/src/ui/slider/slider.tokens.ts +189 -0
- package/src/ui/slider/slider.tsx +259 -0
- package/src/ui/slider/slider.types.ts +288 -0
- package/src/ui/snackbar/index.ts +20 -0
- package/src/ui/snackbar/snackbar.test.tsx +338 -0
- package/src/ui/snackbar/snackbar.tsx +476 -0
- package/{dist/ui/switch/index.d.ts → src/ui/switch/index.ts} +1 -0
- package/src/ui/switch/switch.stories.tsx +309 -0
- package/src/ui/switch/switch.test.tsx +243 -0
- package/src/ui/switch/switch.tokens.ts +89 -0
- package/src/ui/switch/switch.tsx +504 -0
- package/src/ui/switch/switch.types.ts +62 -0
- package/{dist/ui/tabs/index.d.ts → src/ui/tabs/index.ts} +8 -1
- package/src/ui/tabs/tab.tsx +407 -0
- package/src/ui/tabs/tabs-content.tsx +89 -0
- package/src/ui/tabs/tabs-list.tsx +146 -0
- package/src/ui/tabs/tabs.test.tsx +290 -0
- package/src/ui/tabs/tabs.tokens.ts +121 -0
- package/src/ui/tabs/tabs.tsx +229 -0
- package/src/ui/tabs/tabs.types.ts +185 -0
- package/{dist/ui/text-field/index.d.ts → src/ui/text-field/index.ts} +8 -1
- package/src/ui/text-field/subcomponents/active-indicator.tsx +67 -0
- package/src/ui/text-field/subcomponents/floating-label.tsx +161 -0
- package/src/ui/text-field/subcomponents/leading-icon.tsx +46 -0
- package/src/ui/text-field/subcomponents/outline-container.tsx +170 -0
- package/src/ui/text-field/subcomponents/prefix-suffix.tsx +59 -0
- package/src/ui/text-field/subcomponents/supporting-text.tsx +145 -0
- package/src/ui/text-field/subcomponents/trailing-icon.tsx +199 -0
- package/src/ui/text-field/text-field.test.tsx +454 -0
- package/src/ui/text-field/text-field.tokens.ts +104 -0
- package/src/ui/text-field/text-field.tsx +548 -0
- package/src/ui/text-field/text-field.types.ts +180 -0
- package/src/ui/theme-provider/index.tsx +215 -0
- package/src/ui/toc.test.tsx +108 -0
- package/src/ui/toc.tsx +172 -0
- package/src/ui/tooltip/plain-tooltip.tsx +63 -0
- package/src/ui/tooltip/rich-tooltip.tsx +94 -0
- package/src/ui/tooltip/tooltip-box.tsx +266 -0
- package/src/ui/tooltip/tooltip-caret-shape.tsx +68 -0
- package/src/ui/tooltip/tooltip.tokens.ts +26 -0
- package/src/ui/tooltip/tooltip.types.ts +70 -0
- package/src/ui/tooltip/use-tooltip-position.ts +208 -0
- package/src/ui/tooltip/use-tooltip-state.ts +41 -0
- package/src/ui/typography/__tests__/typography.test.tsx +170 -0
- package/{dist/ui/typography/index.d.ts → src/ui/typography/index.ts} +21 -3
- package/src/ui/typography/type-scale-tokens.ts +205 -0
- package/src/ui/typography/typography-key-tokens.ts +43 -0
- package/src/ui/typography/typography-tokens.ts +360 -0
- package/src/ui/typography/typography.css +22 -0
- package/src/ui/typography/typography.tsx +559 -0
- package/test-render.tsx +4 -0
- package/test-shadow.html +26 -0
- package/test_output.txt +164 -0
- package/test_output_v2.txt +5 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +20 -0
- package/vitest.config.ts +11 -0
- package/dist/hooks/useClickOutside.d.ts +0 -8
- package/dist/hooks/useMediaQuery.d.ts +0 -11
- package/dist/hooks/useRipple.d.ts +0 -26
- package/dist/lib/material-symbols-preconnect.d.ts +0 -42
- package/dist/lib/theme-utils.d.ts +0 -63
- package/dist/lib/utils.d.ts +0 -2
- package/dist/types/index.d.ts +0 -1
- package/dist/types/md3.d.ts +0 -14
- package/dist/ui/app-bar/app-bar-column.d.ts +0 -28
- package/dist/ui/app-bar/app-bar-item-button.d.ts +0 -16
- package/dist/ui/app-bar/app-bar-overflow-indicator.d.ts +0 -18
- package/dist/ui/app-bar/app-bar-row.d.ts +0 -36
- package/dist/ui/app-bar/app-bar.tokens.d.ts +0 -184
- package/dist/ui/app-bar/app-bar.types.d.ts +0 -392
- package/dist/ui/app-bar/bottom-app-bar.d.ts +0 -31
- package/dist/ui/app-bar/docked-toolbar.d.ts +0 -25
- package/dist/ui/app-bar/hooks/use-app-bar-scroll.d.ts +0 -42
- package/dist/ui/app-bar/hooks/use-flexible-app-bar.d.ts +0 -37
- package/dist/ui/app-bar/large-flexible-app-bar.d.ts +0 -26
- package/dist/ui/app-bar/medium-flexible-app-bar.d.ts +0 -28
- package/dist/ui/app-bar/search-app-bar.d.ts +0 -43
- package/dist/ui/app-bar/search-view.d.ts +0 -54
- package/dist/ui/app-bar/small-app-bar.d.ts +0 -37
- package/dist/ui/badge.d.ts +0 -125
- package/dist/ui/button-group.d.ts +0 -59
- package/dist/ui/button.d.ts +0 -148
- package/dist/ui/card.d.ts +0 -62
- package/dist/ui/checkbox.d.ts +0 -82
- package/dist/ui/chip.d.ts +0 -110
- package/dist/ui/code-block.d.ts +0 -14
- package/dist/ui/dialog.d.ts +0 -111
- package/dist/ui/divider.d.ts +0 -164
- package/dist/ui/drawer.d.ts +0 -39
- package/dist/ui/dropdown.d.ts +0 -29
- package/dist/ui/fab-menu.d.ts +0 -204
- package/dist/ui/fab.d.ts +0 -162
- package/dist/ui/icon-button.d.ts +0 -131
- package/dist/ui/icon.d.ts +0 -88
- package/dist/ui/loading-indicator.d.ts +0 -42
- package/dist/ui/navigation-rail.d.ts +0 -29
- package/dist/ui/progress-indicator/circular.d.ts +0 -3
- package/dist/ui/progress-indicator/hooks.d.ts +0 -3
- package/dist/ui/progress-indicator/linear-flat.d.ts +0 -10
- package/dist/ui/progress-indicator/linear-wavy.d.ts +0 -18
- package/dist/ui/progress-indicator/linear.d.ts +0 -3
- package/dist/ui/progress-indicator/types.d.ts +0 -151
- package/dist/ui/progress-indicator/utils.d.ts +0 -3
- package/dist/ui/radio-button.d.ts +0 -106
- package/dist/ui/ripple.d.ts +0 -126
- package/dist/ui/scroll-area.d.ts +0 -27
- package/dist/ui/search/animated-placeholder.d.ts +0 -54
- package/dist/ui/search/hooks/use-search-keyboard.d.ts +0 -32
- package/dist/ui/search/hooks/use-search-view-focus.d.ts +0 -6
- package/dist/ui/search/index.d.ts +0 -27
- package/dist/ui/search/search-bar.d.ts +0 -32
- package/dist/ui/search/search-context.d.ts +0 -24
- package/dist/ui/search/search-view-docked.d.ts +0 -25
- package/dist/ui/search/search-view-fullscreen.d.ts +0 -36
- package/dist/ui/search/search.d.ts +0 -50
- package/dist/ui/search/search.tokens.d.ts +0 -112
- package/dist/ui/search/search.types.d.ts +0 -131
- package/dist/ui/search/trailing-action.d.ts +0 -9
- package/dist/ui/shared/constants.d.ts +0 -86
- package/dist/ui/slider/hooks/useSliderMath.d.ts +0 -101
- package/dist/ui/slider/range-slider.d.ts +0 -47
- package/dist/ui/slider/slider-thumb.d.ts +0 -33
- package/dist/ui/slider/slider-track.d.ts +0 -25
- package/dist/ui/slider/slider.d.ts +0 -60
- package/dist/ui/slider/slider.tokens.d.ts +0 -151
- package/dist/ui/slider/slider.types.d.ts +0 -259
- package/dist/ui/snackbar/index.d.ts +0 -6
- package/dist/ui/snackbar/snackbar.d.ts +0 -197
- package/dist/ui/switch/switch.d.ts +0 -30
- package/dist/ui/switch/switch.stories.d.ts +0 -48
- package/dist/ui/switch/switch.tokens.d.ts +0 -67
- package/dist/ui/switch/switch.types.d.ts +0 -59
- package/dist/ui/tabs/tab.d.ts +0 -43
- package/dist/ui/tabs/tabs-content.d.ts +0 -36
- package/dist/ui/tabs/tabs-list.d.ts +0 -40
- package/dist/ui/tabs/tabs.d.ts +0 -60
- package/dist/ui/tabs/tabs.tokens.d.ts +0 -94
- package/dist/ui/tabs/tabs.types.d.ts +0 -172
- package/dist/ui/text-field/subcomponents/active-indicator.d.ts +0 -24
- package/dist/ui/text-field/subcomponents/floating-label.d.ts +0 -43
- package/dist/ui/text-field/subcomponents/leading-icon.d.ts +0 -23
- package/dist/ui/text-field/subcomponents/outline-container.d.ts +0 -42
- package/dist/ui/text-field/subcomponents/prefix-suffix.d.ts +0 -24
- package/dist/ui/text-field/subcomponents/supporting-text.d.ts +0 -37
- package/dist/ui/text-field/subcomponents/trailing-icon.d.ts +0 -41
- package/dist/ui/text-field/text-field.d.ts +0 -49
- package/dist/ui/text-field/text-field.tokens.d.ts +0 -76
- package/dist/ui/text-field/text-field.types.d.ts +0 -126
- package/dist/ui/theme-provider/index.d.ts +0 -48
- package/dist/ui/toc.d.ts +0 -80
- package/dist/ui/tooltip/plain-tooltip.d.ts +0 -2
- package/dist/ui/tooltip/rich-tooltip.d.ts +0 -2
- package/dist/ui/tooltip/tooltip-box.d.ts +0 -2
- package/dist/ui/tooltip/tooltip-caret-shape.d.ts +0 -9
- package/dist/ui/tooltip/tooltip.tokens.d.ts +0 -26
- package/dist/ui/tooltip/tooltip.types.d.ts +0 -56
- package/dist/ui/tooltip/use-tooltip-position.d.ts +0 -8
- package/dist/ui/tooltip/use-tooltip-state.d.ts +0 -2
- package/dist/ui/typography/type-scale-tokens.d.ts +0 -162
- package/dist/ui/typography/typography-key-tokens.d.ts +0 -40
- package/dist/ui/typography/typography-tokens.d.ts +0 -220
- package/dist/ui/typography/typography.d.ts +0 -265
- /package/{dist/hooks/index.d.ts → src/hooks/index.ts} +0 -0
- /package/{dist/ui/tooltip/index.d.ts → src/ui/tooltip/index.ts} +0 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { RadioButton, RadioGroup } from "./radio-button";
|
|
5
|
+
|
|
6
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/** Renders a basic RadioGroup with 3 options. */
|
|
9
|
+
function renderGroup(props: {
|
|
10
|
+
value?: string;
|
|
11
|
+
defaultValue?: string;
|
|
12
|
+
onValueChange?: (val: string) => void;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}) {
|
|
15
|
+
return render(
|
|
16
|
+
<RadioGroup name="test-group" {...props} label="Test Group">
|
|
17
|
+
<RadioButton value="a" label="Option A" />
|
|
18
|
+
<RadioButton value="b" label="Option B" />
|
|
19
|
+
<RadioButton value="c" label="Option C" />
|
|
20
|
+
</RadioGroup>,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe("RadioButton Component", () => {
|
|
27
|
+
// ── 1. Rendering ──────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe("rendering", () => {
|
|
30
|
+
it("renders without label", () => {
|
|
31
|
+
render(<RadioButton value="x" aria-label="Option X" />);
|
|
32
|
+
const input = screen.getByRole("radio");
|
|
33
|
+
expect(input).toBeInTheDocument();
|
|
34
|
+
expect(input).toHaveAttribute("type", "radio");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders with label", () => {
|
|
38
|
+
render(<RadioButton value="x" label="My Option" />);
|
|
39
|
+
expect(screen.getByRole("radio")).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText("My Option")).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("is unselected by default", () => {
|
|
44
|
+
render(<RadioButton value="x" aria-label="Option X" />);
|
|
45
|
+
expect(screen.getByRole("radio")).not.toBeChecked();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders as selected when selected prop is true", () => {
|
|
49
|
+
render(
|
|
50
|
+
<RadioButton
|
|
51
|
+
value="x"
|
|
52
|
+
selected
|
|
53
|
+
aria-label="Option X"
|
|
54
|
+
onClick={vi.fn()}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
expect(screen.getByRole("radio")).toBeChecked();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("renders as unselected when selected prop is false", () => {
|
|
61
|
+
render(
|
|
62
|
+
<RadioButton
|
|
63
|
+
value="x"
|
|
64
|
+
selected={false}
|
|
65
|
+
aria-label="Option X"
|
|
66
|
+
onClick={vi.fn()}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
69
|
+
expect(screen.getByRole("radio")).not.toBeChecked();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ── 2. Selection (Controlled) ─────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
describe("controlled selection", () => {
|
|
76
|
+
it("fires onClick when clicked", () => {
|
|
77
|
+
const handleClick = vi.fn();
|
|
78
|
+
render(
|
|
79
|
+
<RadioButton value="x" aria-label="Option X" onClick={handleClick} />,
|
|
80
|
+
);
|
|
81
|
+
fireEvent.click(screen.getByRole("radio"));
|
|
82
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("does not fire onClick when onClick is null", () => {
|
|
86
|
+
const handleClick = vi.fn();
|
|
87
|
+
render(<RadioButton value="x" aria-label="Option X" onClick={null} />);
|
|
88
|
+
fireEvent.click(screen.getByRole("radio"));
|
|
89
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("reflects controlled selected state", () => {
|
|
93
|
+
const { rerender } = render(
|
|
94
|
+
<RadioButton
|
|
95
|
+
value="x"
|
|
96
|
+
selected={false}
|
|
97
|
+
aria-label="Option X"
|
|
98
|
+
onClick={vi.fn()}
|
|
99
|
+
/>,
|
|
100
|
+
);
|
|
101
|
+
expect(screen.getByRole("radio")).not.toBeChecked();
|
|
102
|
+
|
|
103
|
+
rerender(
|
|
104
|
+
<RadioButton
|
|
105
|
+
value="x"
|
|
106
|
+
selected={true}
|
|
107
|
+
aria-label="Option X"
|
|
108
|
+
onClick={vi.fn()}
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
expect(screen.getByRole("radio")).toBeChecked();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ── 3. Selection (Uncontrolled) ───────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
describe("uncontrolled selection", () => {
|
|
118
|
+
it("starts unselected with no defaultSelected", () => {
|
|
119
|
+
render(<RadioButton value="x" aria-label="Option X" />);
|
|
120
|
+
expect(screen.getByRole("radio")).not.toBeChecked();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("starts selected when defaultSelected is true", () => {
|
|
124
|
+
render(<RadioButton value="x" defaultSelected aria-label="Option X" />);
|
|
125
|
+
expect(screen.getByRole("radio")).toBeChecked();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("updates internal state on click", () => {
|
|
129
|
+
render(<RadioButton value="x" aria-label="Option X" />);
|
|
130
|
+
const input = screen.getByRole("radio");
|
|
131
|
+
expect(input).not.toBeChecked();
|
|
132
|
+
|
|
133
|
+
fireEvent.click(input);
|
|
134
|
+
expect(input).toBeChecked();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── 4. Disabled state ─────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
describe("disabled state", () => {
|
|
141
|
+
it("has disabled attribute when disabled", () => {
|
|
142
|
+
render(<RadioButton value="x" disabled aria-label="Option X" />);
|
|
143
|
+
expect(screen.getByRole("radio")).toBeDisabled();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("sets aria-disabled on the input", () => {
|
|
147
|
+
render(<RadioButton value="x" disabled aria-label="Option X" />);
|
|
148
|
+
expect(screen.getByRole("radio")).toHaveAttribute(
|
|
149
|
+
"aria-disabled",
|
|
150
|
+
"true",
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("does not fire onClick when disabled", () => {
|
|
155
|
+
const handleClick = vi.fn();
|
|
156
|
+
render(
|
|
157
|
+
<RadioButton
|
|
158
|
+
value="x"
|
|
159
|
+
disabled
|
|
160
|
+
aria-label="Option X"
|
|
161
|
+
onClick={handleClick}
|
|
162
|
+
/>,
|
|
163
|
+
);
|
|
164
|
+
fireEvent.click(screen.getByRole("radio"));
|
|
165
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ── 5. RadioGroup behavior ────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
describe("RadioGroup", () => {
|
|
172
|
+
it("only one radio selected at a time (controlled)", () => {
|
|
173
|
+
const handleChange = vi.fn();
|
|
174
|
+
renderGroup({ value: "a", onValueChange: handleChange });
|
|
175
|
+
|
|
176
|
+
const radios = screen.getAllByRole("radio");
|
|
177
|
+
expect(radios[0]).toBeChecked();
|
|
178
|
+
expect(radios[1]).not.toBeChecked();
|
|
179
|
+
expect(radios[2]).not.toBeChecked();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("fires onValueChange with correct value when selecting", () => {
|
|
183
|
+
const handleChange = vi.fn();
|
|
184
|
+
renderGroup({ value: "a", onValueChange: handleChange });
|
|
185
|
+
|
|
186
|
+
fireEvent.click(screen.getAllByRole("radio")[1]);
|
|
187
|
+
expect(handleChange).toHaveBeenCalledWith("b");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("works in uncontrolled mode with defaultValue", () => {
|
|
191
|
+
renderGroup({ defaultValue: "b" });
|
|
192
|
+
const radios = screen.getAllByRole("radio");
|
|
193
|
+
expect(radios[1]).toBeChecked();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("disables all radios when group is disabled", () => {
|
|
197
|
+
renderGroup({ disabled: true });
|
|
198
|
+
const radios = screen.getAllByRole("radio");
|
|
199
|
+
for (const radio of radios) {
|
|
200
|
+
expect(radio).toBeDisabled();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("shares the same name across all radios", () => {
|
|
205
|
+
renderGroup({});
|
|
206
|
+
const radios = screen.getAllByRole("radio");
|
|
207
|
+
for (const radio of radios) {
|
|
208
|
+
expect(radio).toHaveAttribute("name", "test-group");
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("updates selection in uncontrolled mode on click", () => {
|
|
213
|
+
renderGroup({ defaultValue: "a" });
|
|
214
|
+
const radios = screen.getAllByRole("radio");
|
|
215
|
+
|
|
216
|
+
expect(radios[0]).toBeChecked();
|
|
217
|
+
fireEvent.click(radios[2]);
|
|
218
|
+
expect(radios[2]).toBeChecked();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ── 6. Keyboard navigation ────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
describe("keyboard navigation", () => {
|
|
225
|
+
it("ArrowDown moves to next radio and selects it", () => {
|
|
226
|
+
const handleChange = vi.fn();
|
|
227
|
+
const { container } = renderGroup({
|
|
228
|
+
value: "a",
|
|
229
|
+
onValueChange: handleChange,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const group = container.querySelector("[role='radiogroup']");
|
|
233
|
+
if (!group) throw new Error("radiogroup not found");
|
|
234
|
+
const radios = screen.getAllByRole("radio");
|
|
235
|
+
radios[0].focus();
|
|
236
|
+
|
|
237
|
+
fireEvent.keyDown(group, { key: "ArrowDown" });
|
|
238
|
+
expect(handleChange).toHaveBeenCalledWith("b");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("ArrowRight moves to next radio", () => {
|
|
242
|
+
const handleChange = vi.fn();
|
|
243
|
+
const { container } = renderGroup({
|
|
244
|
+
value: "a",
|
|
245
|
+
onValueChange: handleChange,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const group = container.querySelector("[role='radiogroup']");
|
|
249
|
+
if (!group) throw new Error("radiogroup not found");
|
|
250
|
+
const radios = screen.getAllByRole("radio");
|
|
251
|
+
radios[0].focus();
|
|
252
|
+
|
|
253
|
+
fireEvent.keyDown(group, { key: "ArrowRight" });
|
|
254
|
+
expect(handleChange).toHaveBeenCalledWith("b");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("ArrowUp moves to previous radio", () => {
|
|
258
|
+
const handleChange = vi.fn();
|
|
259
|
+
const { container } = renderGroup({
|
|
260
|
+
value: "b",
|
|
261
|
+
onValueChange: handleChange,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const group = container.querySelector("[role='radiogroup']");
|
|
265
|
+
if (!group) throw new Error("radiogroup not found");
|
|
266
|
+
const radios = screen.getAllByRole("radio");
|
|
267
|
+
radios[1].focus();
|
|
268
|
+
|
|
269
|
+
fireEvent.keyDown(group, { key: "ArrowUp" });
|
|
270
|
+
expect(handleChange).toHaveBeenCalledWith("a");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("ArrowLeft moves to previous radio", () => {
|
|
274
|
+
const handleChange = vi.fn();
|
|
275
|
+
const { container } = renderGroup({
|
|
276
|
+
value: "b",
|
|
277
|
+
onValueChange: handleChange,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const group = container.querySelector("[role='radiogroup']");
|
|
281
|
+
if (!group) throw new Error("radiogroup not found");
|
|
282
|
+
const radios = screen.getAllByRole("radio");
|
|
283
|
+
radios[1].focus();
|
|
284
|
+
|
|
285
|
+
fireEvent.keyDown(group, { key: "ArrowLeft" });
|
|
286
|
+
expect(handleChange).toHaveBeenCalledWith("a");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("wraps from last to first with ArrowDown", () => {
|
|
290
|
+
const handleChange = vi.fn();
|
|
291
|
+
const { container } = renderGroup({
|
|
292
|
+
value: "c",
|
|
293
|
+
onValueChange: handleChange,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const group = container.querySelector("[role='radiogroup']");
|
|
297
|
+
if (!group) throw new Error("radiogroup not found");
|
|
298
|
+
const radios = screen.getAllByRole("radio");
|
|
299
|
+
radios[2].focus();
|
|
300
|
+
|
|
301
|
+
fireEvent.keyDown(group, { key: "ArrowDown" });
|
|
302
|
+
expect(handleChange).toHaveBeenCalledWith("a");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("wraps from first to last with ArrowUp", () => {
|
|
306
|
+
const handleChange = vi.fn();
|
|
307
|
+
const { container } = renderGroup({
|
|
308
|
+
value: "a",
|
|
309
|
+
onValueChange: handleChange,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const group = container.querySelector("[role='radiogroup']");
|
|
313
|
+
if (!group) throw new Error("radiogroup not found");
|
|
314
|
+
const radios = screen.getAllByRole("radio");
|
|
315
|
+
radios[0].focus();
|
|
316
|
+
|
|
317
|
+
fireEvent.keyDown(group, { key: "ArrowUp" });
|
|
318
|
+
expect(handleChange).toHaveBeenCalledWith("c");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("does not navigate when group is disabled", () => {
|
|
322
|
+
const handleChange = vi.fn();
|
|
323
|
+
const { container } = renderGroup({
|
|
324
|
+
value: "a",
|
|
325
|
+
onValueChange: handleChange,
|
|
326
|
+
disabled: true,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const group = container.querySelector("[role='radiogroup']");
|
|
330
|
+
if (!group) throw new Error("radiogroup not found");
|
|
331
|
+
fireEvent.keyDown(group, { key: "ArrowDown" });
|
|
332
|
+
expect(handleChange).not.toHaveBeenCalled();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ── 7. Accessibility ──────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
describe("accessibility", () => {
|
|
339
|
+
it("input has role radio", () => {
|
|
340
|
+
render(<RadioButton value="x" aria-label="Option X" />);
|
|
341
|
+
expect(screen.getByRole("radio")).toBeInTheDocument();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("has aria-label when provided", () => {
|
|
345
|
+
render(<RadioButton value="x" aria-label="My Radio Label" />);
|
|
346
|
+
expect(screen.getByRole("radio")).toHaveAttribute(
|
|
347
|
+
"aria-label",
|
|
348
|
+
"My Radio Label",
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("RadioGroup has role radiogroup", () => {
|
|
353
|
+
renderGroup({});
|
|
354
|
+
expect(screen.getByRole("radiogroup")).toBeInTheDocument();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("RadioGroup sets aria-disabled when disabled", () => {
|
|
358
|
+
renderGroup({ disabled: true });
|
|
359
|
+
expect(screen.getByRole("radiogroup")).toHaveAttribute(
|
|
360
|
+
"aria-disabled",
|
|
361
|
+
"true",
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("RadioGroup label is accessible via aria-label", () => {
|
|
366
|
+
renderGroup({});
|
|
367
|
+
const group = screen.getByRole("radiogroup");
|
|
368
|
+
expect(group).toHaveAttribute("aria-label", "Test Group");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("RadioButton label associates with input via htmlFor", () => {
|
|
372
|
+
render(<RadioButton value="x" label="Labeled Option" />);
|
|
373
|
+
const input = screen.getByRole("radio");
|
|
374
|
+
const label = screen.getByText("Labeled Option").closest("label");
|
|
375
|
+
expect(label).toHaveAttribute("for", input.id);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("input is visually hidden (sr-only)", () => {
|
|
379
|
+
render(<RadioButton value="x" aria-label="Option X" />);
|
|
380
|
+
const input = screen.getByRole("radio");
|
|
381
|
+
expect(input.className).toContain("sr-only");
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ── 8. Ref forwarding ─────────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
describe("ref forwarding", () => {
|
|
388
|
+
it("ref points to the hidden input element", () => {
|
|
389
|
+
const ref = React.createRef<HTMLInputElement>();
|
|
390
|
+
render(<RadioButton value="x" aria-label="Option X" ref={ref} />);
|
|
391
|
+
expect(ref.current).toBeTruthy();
|
|
392
|
+
if (!ref.current) throw new Error("ref.current is null");
|
|
393
|
+
expect(ref.current.tagName.toLowerCase()).toBe("input");
|
|
394
|
+
expect(ref.current.type).toBe("radio");
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// ── 9. Reduced motion ─────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
describe("reduced motion", () => {
|
|
401
|
+
it("renders correctly when reduced motion is preferred", () => {
|
|
402
|
+
// RadioVisual internal renders; just check component doesn't crash
|
|
403
|
+
render(<RadioButton value="x" aria-label="Option X" selected />);
|
|
404
|
+
expect(screen.getByRole("radio")).toBeChecked();
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|