@bug-on/md3-react 2.0.3 → 3.0.0
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 +33 -0
- package/CHANGELOG.md +55 -0
- package/dist/index.css.d.ts +2 -0
- package/dist/index.d.mts +6127 -0
- package/dist/index.d.ts +6127 -71
- package/dist/index.js +1653 -614
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1566 -547
- 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/typography.css.d.ts +2 -0
- package/package.json +22 -19
- package/scripts/copy-assets.js +82 -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 +180 -0
- package/src/lib/utils.ts +6 -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 +297 -0
- package/src/ui/button.tsx +669 -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 +604 -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 +122 -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 +190 -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,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file checkbox.test.tsx
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive test suite for the MD3 Expressive Checkbox component.
|
|
5
|
+
* Tests cover: rendering, controlled state, tri-state, error, disabled,
|
|
6
|
+
* accessibility, form integration, and keyboard interaction.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
10
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { Checkbox, TriStateCheckbox } from "./checkbox";
|
|
12
|
+
|
|
13
|
+
afterEach(cleanup);
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// Rendering
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
describe("Checkbox — Rendering", () => {
|
|
20
|
+
it("renders unchecked by default", () => {
|
|
21
|
+
render(<Checkbox aria-label="Test checkbox" />);
|
|
22
|
+
const input = screen.getByRole("checkbox", { name: "Test checkbox" });
|
|
23
|
+
expect(input).not.toBeChecked();
|
|
24
|
+
expect(input).toHaveAttribute("aria-checked", "false");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("renders checked when checked=true", () => {
|
|
28
|
+
render(<Checkbox checked aria-label="Test" onCheckedChange={() => {}} />);
|
|
29
|
+
const input = screen.getByRole("checkbox", { name: "Test" });
|
|
30
|
+
expect(input).toBeChecked();
|
|
31
|
+
expect(input).toHaveAttribute("aria-checked", "true");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("renders indeterminate when indeterminate=true", () => {
|
|
35
|
+
render(<Checkbox indeterminate aria-label="Test" />);
|
|
36
|
+
const input = screen.getByRole("checkbox", { name: "Test" });
|
|
37
|
+
expect(input).toHaveAttribute("aria-checked", "mixed");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("renders with label text", () => {
|
|
41
|
+
render(<Checkbox label="Accept terms" />);
|
|
42
|
+
expect(screen.getByText("Accept terms")).toBeInTheDocument();
|
|
43
|
+
// Label should be associated with the checkbox
|
|
44
|
+
const input = screen.getByRole("checkbox");
|
|
45
|
+
expect(input).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders without label when label prop is omitted", () => {
|
|
49
|
+
render(<Checkbox aria-label="Standalone" />);
|
|
50
|
+
expect(screen.queryByText("Accept terms")).not.toBeInTheDocument();
|
|
51
|
+
expect(
|
|
52
|
+
screen.getByRole("checkbox", { name: "Standalone" }),
|
|
53
|
+
).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("applies custom className to the wrapper", () => {
|
|
57
|
+
render(<Checkbox aria-label="Test" className="custom-class" />);
|
|
58
|
+
// The wrapper div should have the custom class
|
|
59
|
+
const wrapper = screen
|
|
60
|
+
.getByRole("checkbox", { name: "Test" })
|
|
61
|
+
.closest(".w-12");
|
|
62
|
+
expect(wrapper).toHaveClass("custom-class");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
// Controlled State
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
describe("Checkbox — Controlled State", () => {
|
|
71
|
+
it("calls onCheckedChange with true when unchecked checkbox is clicked", () => {
|
|
72
|
+
const onCheckedChange = vi.fn();
|
|
73
|
+
render(
|
|
74
|
+
<Checkbox
|
|
75
|
+
checked={false}
|
|
76
|
+
onCheckedChange={onCheckedChange}
|
|
77
|
+
aria-label="Test"
|
|
78
|
+
/>,
|
|
79
|
+
);
|
|
80
|
+
const input = screen.getByRole("checkbox");
|
|
81
|
+
fireEvent.click(input);
|
|
82
|
+
expect(onCheckedChange).toHaveBeenCalledOnce();
|
|
83
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("calls onCheckedChange with false when checked checkbox is clicked", () => {
|
|
87
|
+
const onCheckedChange = vi.fn();
|
|
88
|
+
render(
|
|
89
|
+
<Checkbox checked onCheckedChange={onCheckedChange} aria-label="Test" />,
|
|
90
|
+
);
|
|
91
|
+
const input = screen.getByRole("checkbox");
|
|
92
|
+
fireEvent.click(input);
|
|
93
|
+
expect(onCheckedChange).toHaveBeenCalledOnce();
|
|
94
|
+
expect(onCheckedChange).toHaveBeenCalledWith(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does not call onCheckedChange when disabled", () => {
|
|
98
|
+
const onCheckedChange = vi.fn();
|
|
99
|
+
render(
|
|
100
|
+
<Checkbox
|
|
101
|
+
checked={false}
|
|
102
|
+
onCheckedChange={onCheckedChange}
|
|
103
|
+
disabled
|
|
104
|
+
aria-label="Test"
|
|
105
|
+
/>,
|
|
106
|
+
);
|
|
107
|
+
const input = screen.getByRole("checkbox");
|
|
108
|
+
fireEvent.click(input);
|
|
109
|
+
expect(onCheckedChange).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
// Tri-State Mode
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
describe("Checkbox — Tri-State", () => {
|
|
118
|
+
it("renders unchecked when state='unchecked'", () => {
|
|
119
|
+
render(
|
|
120
|
+
<Checkbox
|
|
121
|
+
state="unchecked"
|
|
122
|
+
onStateChange={() => {}}
|
|
123
|
+
aria-label="Parent"
|
|
124
|
+
/>,
|
|
125
|
+
);
|
|
126
|
+
const input = screen.getByRole("checkbox");
|
|
127
|
+
expect(input).toHaveAttribute("aria-checked", "false");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("renders checked when state='checked'", () => {
|
|
131
|
+
render(
|
|
132
|
+
<Checkbox state="checked" onStateChange={() => {}} aria-label="Parent" />,
|
|
133
|
+
);
|
|
134
|
+
const input = screen.getByRole("checkbox");
|
|
135
|
+
expect(input).toHaveAttribute("aria-checked", "true");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("renders indeterminate when state='indeterminate'", () => {
|
|
139
|
+
render(
|
|
140
|
+
<Checkbox
|
|
141
|
+
state="indeterminate"
|
|
142
|
+
onStateChange={() => {}}
|
|
143
|
+
aria-label="Parent"
|
|
144
|
+
/>,
|
|
145
|
+
);
|
|
146
|
+
const input = screen.getByRole("checkbox");
|
|
147
|
+
expect(input).toHaveAttribute("aria-checked", "mixed");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("calls onStateChange with next state on click (unchecked → checked)", () => {
|
|
151
|
+
const onStateChange = vi.fn();
|
|
152
|
+
render(
|
|
153
|
+
<Checkbox
|
|
154
|
+
state="unchecked"
|
|
155
|
+
onStateChange={onStateChange}
|
|
156
|
+
aria-label="Parent"
|
|
157
|
+
/>,
|
|
158
|
+
);
|
|
159
|
+
fireEvent.click(screen.getByRole("checkbox"));
|
|
160
|
+
expect(onStateChange).toHaveBeenCalledWith("checked");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("calls onStateChange with next state on click (checked → indeterminate)", () => {
|
|
164
|
+
const onStateChange = vi.fn();
|
|
165
|
+
render(
|
|
166
|
+
<Checkbox
|
|
167
|
+
state="checked"
|
|
168
|
+
onStateChange={onStateChange}
|
|
169
|
+
aria-label="Parent"
|
|
170
|
+
/>,
|
|
171
|
+
);
|
|
172
|
+
fireEvent.click(screen.getByRole("checkbox"));
|
|
173
|
+
expect(onStateChange).toHaveBeenCalledWith("indeterminate");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("calls onStateChange with next state on click (indeterminate → unchecked)", () => {
|
|
177
|
+
const onStateChange = vi.fn();
|
|
178
|
+
render(
|
|
179
|
+
<Checkbox
|
|
180
|
+
state="indeterminate"
|
|
181
|
+
onStateChange={onStateChange}
|
|
182
|
+
aria-label="Parent"
|
|
183
|
+
/>,
|
|
184
|
+
);
|
|
185
|
+
fireEvent.click(screen.getByRole("checkbox"));
|
|
186
|
+
expect(onStateChange).toHaveBeenCalledWith("unchecked");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
191
|
+
// TriStateCheckbox component
|
|
192
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
describe("TriStateCheckbox", () => {
|
|
195
|
+
it("renders with correct state", () => {
|
|
196
|
+
render(
|
|
197
|
+
<TriStateCheckbox
|
|
198
|
+
state="indeterminate"
|
|
199
|
+
onStateChange={() => {}}
|
|
200
|
+
aria-label="Select all"
|
|
201
|
+
/>,
|
|
202
|
+
);
|
|
203
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
204
|
+
"aria-checked",
|
|
205
|
+
"mixed",
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("calls onStateChange when clicked", () => {
|
|
210
|
+
const onStateChange = vi.fn();
|
|
211
|
+
render(
|
|
212
|
+
<TriStateCheckbox
|
|
213
|
+
state="unchecked"
|
|
214
|
+
onStateChange={onStateChange}
|
|
215
|
+
aria-label="Select all"
|
|
216
|
+
/>,
|
|
217
|
+
);
|
|
218
|
+
fireEvent.click(screen.getByRole("checkbox"));
|
|
219
|
+
expect(onStateChange).toHaveBeenCalledWith("checked");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
224
|
+
// Error State
|
|
225
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
describe("Checkbox — Error State", () => {
|
|
228
|
+
it("sets aria-invalid=true in error state", () => {
|
|
229
|
+
render(<Checkbox error aria-label="Required" />);
|
|
230
|
+
const input = screen.getByRole("checkbox");
|
|
231
|
+
expect(input).toHaveAttribute("aria-invalid", "true");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("does not set aria-invalid when error is false", () => {
|
|
235
|
+
render(<Checkbox aria-label="Test" />);
|
|
236
|
+
const input = screen.getByRole("checkbox");
|
|
237
|
+
expect(input).not.toHaveAttribute("aria-invalid");
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
242
|
+
// Disabled State
|
|
243
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
describe("Checkbox — Disabled State", () => {
|
|
246
|
+
it("has disabled attribute when disabled=true", () => {
|
|
247
|
+
render(<Checkbox disabled aria-label="Disabled checkbox" />);
|
|
248
|
+
expect(screen.getByRole("checkbox")).toBeDisabled();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("has aria-disabled=true when disabled=true", () => {
|
|
252
|
+
render(<Checkbox disabled aria-label="Disabled checkbox" />);
|
|
253
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
254
|
+
"aria-disabled",
|
|
255
|
+
"true",
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("does not call onCheckedChange when disabled and clicked", () => {
|
|
260
|
+
const onCheckedChange = vi.fn();
|
|
261
|
+
render(
|
|
262
|
+
<Checkbox
|
|
263
|
+
disabled
|
|
264
|
+
onCheckedChange={onCheckedChange}
|
|
265
|
+
aria-label="Disabled"
|
|
266
|
+
/>,
|
|
267
|
+
);
|
|
268
|
+
fireEvent.click(screen.getByRole("checkbox"));
|
|
269
|
+
expect(onCheckedChange).not.toHaveBeenCalled();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("does not call onStateChange when disabled (tri-state)", () => {
|
|
273
|
+
const onStateChange = vi.fn();
|
|
274
|
+
render(
|
|
275
|
+
<Checkbox
|
|
276
|
+
disabled
|
|
277
|
+
state="unchecked"
|
|
278
|
+
onStateChange={onStateChange}
|
|
279
|
+
aria-label="Disabled"
|
|
280
|
+
/>,
|
|
281
|
+
);
|
|
282
|
+
fireEvent.click(screen.getByRole("checkbox"));
|
|
283
|
+
expect(onStateChange).not.toHaveBeenCalled();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
288
|
+
// Accessibility
|
|
289
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
describe("Checkbox — Accessibility", () => {
|
|
292
|
+
it("has correct aria-checked='false' when unchecked", () => {
|
|
293
|
+
render(<Checkbox aria-label="Test" />);
|
|
294
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
295
|
+
"aria-checked",
|
|
296
|
+
"false",
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("has correct aria-checked='true' when checked", () => {
|
|
301
|
+
render(<Checkbox checked aria-label="Test" onCheckedChange={() => {}} />);
|
|
302
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
303
|
+
"aria-checked",
|
|
304
|
+
"true",
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("has correct aria-checked='mixed' when indeterminate", () => {
|
|
309
|
+
render(<Checkbox indeterminate aria-label="Test" />);
|
|
310
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
311
|
+
"aria-checked",
|
|
312
|
+
"mixed",
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("associates label with input via htmlFor when label prop provided", () => {
|
|
317
|
+
render(<Checkbox label="My label" id="my-checkbox" />);
|
|
318
|
+
const label = screen.getByText("My label").closest("label");
|
|
319
|
+
const input = screen.getByRole("checkbox");
|
|
320
|
+
expect(label).toHaveAttribute("for", "my-checkbox");
|
|
321
|
+
expect(input).toHaveAttribute("id", "my-checkbox");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("forwards ref to the hidden input element", () => {
|
|
325
|
+
const ref = { current: null } as React.RefObject<HTMLInputElement | null>;
|
|
326
|
+
render(<Checkbox ref={ref} aria-label="Test" />);
|
|
327
|
+
expect(ref.current).not.toBeNull();
|
|
328
|
+
expect(ref.current?.tagName).toBe("INPUT");
|
|
329
|
+
expect(ref.current?.type).toBe("checkbox");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("supports aria-label prop", () => {
|
|
333
|
+
render(<Checkbox aria-label="Custom accessible label" />);
|
|
334
|
+
expect(
|
|
335
|
+
screen.getByRole("checkbox", { name: "Custom accessible label" }),
|
|
336
|
+
).toBeInTheDocument();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("supports aria-labelledby prop", () => {
|
|
340
|
+
render(
|
|
341
|
+
<>
|
|
342
|
+
<span id="label-text">External label</span>
|
|
343
|
+
<Checkbox aria-labelledby="label-text" />
|
|
344
|
+
</>,
|
|
345
|
+
);
|
|
346
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
347
|
+
"aria-labelledby",
|
|
348
|
+
"label-text",
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("supports aria-describedby prop", () => {
|
|
353
|
+
render(
|
|
354
|
+
<>
|
|
355
|
+
<span id="desc">Description text</span>
|
|
356
|
+
<Checkbox aria-label="Test" aria-describedby="desc" />
|
|
357
|
+
</>,
|
|
358
|
+
);
|
|
359
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
360
|
+
"aria-describedby",
|
|
361
|
+
"desc",
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("supports aria-required prop", () => {
|
|
366
|
+
render(<Checkbox aria-label="Required field" aria-required />);
|
|
367
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
|
368
|
+
"aria-required",
|
|
369
|
+
"true",
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
375
|
+
// Form Integration
|
|
376
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
describe("Checkbox — Form Integration", () => {
|
|
379
|
+
it("renders with name prop on the input", () => {
|
|
380
|
+
render(<Checkbox name="newsletter" aria-label="Subscribe" />);
|
|
381
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute("name", "newsletter");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("renders with value prop on the input", () => {
|
|
385
|
+
render(<Checkbox value="yes" aria-label="Subscribe" />);
|
|
386
|
+
expect(screen.getByRole("checkbox")).toHaveAttribute("value", "yes");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("works as uncontrolled with defaultChecked=true", () => {
|
|
390
|
+
render(<Checkbox defaultChecked aria-label="Uncontrolled" />);
|
|
391
|
+
expect(screen.getByRole("checkbox")).toBeChecked();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("toggles state when uncontrolled", () => {
|
|
395
|
+
render(<Checkbox aria-label="Uncontrolled toggle" />);
|
|
396
|
+
const input = screen.getByRole("checkbox");
|
|
397
|
+
expect(input).toHaveAttribute("aria-checked", "false");
|
|
398
|
+
fireEvent.click(input);
|
|
399
|
+
expect(input).toHaveAttribute("aria-checked", "true");
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
404
|
+
// Keyboard Interaction
|
|
405
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
describe("Checkbox — Keyboard Interaction", () => {
|
|
408
|
+
it("toggles on Space key press", () => {
|
|
409
|
+
const onCheckedChange = vi.fn();
|
|
410
|
+
render(
|
|
411
|
+
<Checkbox
|
|
412
|
+
checked={false}
|
|
413
|
+
onCheckedChange={onCheckedChange}
|
|
414
|
+
aria-label="Space test"
|
|
415
|
+
/>,
|
|
416
|
+
);
|
|
417
|
+
const input = screen.getByRole("checkbox");
|
|
418
|
+
input.focus();
|
|
419
|
+
fireEvent.keyDown(input, { key: " ", code: "Space" });
|
|
420
|
+
fireEvent.click(input);
|
|
421
|
+
expect(onCheckedChange).toHaveBeenCalledWith(true);
|
|
422
|
+
});
|
|
423
|
+
});
|