@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,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file slider.types.ts
|
|
3
|
+
* MD3 Expressive Slider — TypeScript prop definitions.
|
|
4
|
+
* Spec: https://m3.material.io/components/sliders/overview
|
|
5
|
+
* Reference: docs/m3/sliders/Slider.kt
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type * as React from "react";
|
|
9
|
+
|
|
10
|
+
// ─── Track Size ───────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** Track size variants mapping to physical px values. */
|
|
13
|
+
export type SliderTrackSize = "xs" | "s" | "m" | "l" | "xl";
|
|
14
|
+
export type SliderVariant = "primary" | "secondary" | "tertiary" | "error";
|
|
15
|
+
|
|
16
|
+
export type SliderTrackShape = "md3" | "full" | number;
|
|
17
|
+
|
|
18
|
+
// ─── Orientation ──────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Slider layout direction. */
|
|
21
|
+
export type SliderOrientation = "horizontal" | "vertical";
|
|
22
|
+
|
|
23
|
+
// ─── Internal Context ────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Internal context shared between Slider sub-components.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export interface SliderContextValue {
|
|
30
|
+
/** Minimum allowed value. */
|
|
31
|
+
min: number;
|
|
32
|
+
/** Maximum allowed value. */
|
|
33
|
+
max: number;
|
|
34
|
+
/**
|
|
35
|
+
* Step size. When > 0, slider is discrete and snaps to multiples of step.
|
|
36
|
+
* When 0, slider is continuous.
|
|
37
|
+
*/
|
|
38
|
+
step: number;
|
|
39
|
+
/** Whether the slider is interactive. */
|
|
40
|
+
disabled: boolean;
|
|
41
|
+
/** Layout orientation. */
|
|
42
|
+
orientation: SliderOrientation;
|
|
43
|
+
/** Physical size of the track. */
|
|
44
|
+
trackSize: SliderTrackSize;
|
|
45
|
+
/** Color variant. */
|
|
46
|
+
variant: SliderVariant;
|
|
47
|
+
/**
|
|
48
|
+
* When true, active track originates from center (50%) instead of the min end.
|
|
49
|
+
* Mirrors Compose's `SliderDefaults.Track(drawCenteredTrack = true)`.
|
|
50
|
+
*/
|
|
51
|
+
isCentered: boolean;
|
|
52
|
+
/** Show the floating value tooltip on hover/drag. */
|
|
53
|
+
showValueIndicator: boolean;
|
|
54
|
+
/** Ref to the track DOM element — used for drag constraint. */
|
|
55
|
+
trackRef: React.RefObject<HTMLDivElement | null>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Slider Props ────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Props for the `<Slider>` component.
|
|
62
|
+
*
|
|
63
|
+
* Supports both controlled (`value` + `onValueChange`) and
|
|
64
|
+
* uncontrolled (`defaultValue`) usage patterns per React standards.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```tsx
|
|
68
|
+
* // Controlled
|
|
69
|
+
* <Slider value={volume} onValueChange={setVolume} min={0} max={100} />
|
|
70
|
+
*
|
|
71
|
+
* // Uncontrolled
|
|
72
|
+
* <Slider defaultValue={50} />
|
|
73
|
+
*
|
|
74
|
+
* // Discrete (step snapping)
|
|
75
|
+
* <Slider defaultValue={0} step={10} />
|
|
76
|
+
*
|
|
77
|
+
* // Vertical orientation
|
|
78
|
+
* <Slider defaultValue={50} orientation="vertical" />
|
|
79
|
+
*
|
|
80
|
+
* // Centered active track
|
|
81
|
+
* <Slider defaultValue={0} isCentered />
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export interface SliderProps {
|
|
85
|
+
/** Controlled current value. Use with `onValueChange`. */
|
|
86
|
+
value?: number;
|
|
87
|
+
/** Initial value for uncontrolled usage. @default midpoint of min/max */
|
|
88
|
+
defaultValue?: number;
|
|
89
|
+
/** Called whenever the value changes during interaction. */
|
|
90
|
+
onValueChange?: (value: number) => void;
|
|
91
|
+
/** Called when the user finishes dragging / commits a keyboard change. */
|
|
92
|
+
onValueChangeEnd?: (value: number) => void;
|
|
93
|
+
/** Minimum value. @default 0 */
|
|
94
|
+
min?: number;
|
|
95
|
+
/** Maximum value. @default 100 */
|
|
96
|
+
max?: number;
|
|
97
|
+
/**
|
|
98
|
+
* Step size. When > 0, slider snaps to multiples of `step` from `min`
|
|
99
|
+
* and renders tick marks. When 0, slider is continuous.
|
|
100
|
+
* @default 0
|
|
101
|
+
*/
|
|
102
|
+
step?: number;
|
|
103
|
+
/** Layout orientation. @default "horizontal" */
|
|
104
|
+
orientation?: SliderOrientation;
|
|
105
|
+
/**
|
|
106
|
+
* Physical track size.
|
|
107
|
+
* Horizontal: height. Vertical: width.
|
|
108
|
+
* @default "m"
|
|
109
|
+
*/
|
|
110
|
+
trackSize?: SliderTrackSize;
|
|
111
|
+
/**
|
|
112
|
+
* Color variant.
|
|
113
|
+
* @default "primary"
|
|
114
|
+
*/
|
|
115
|
+
variant?: SliderVariant;
|
|
116
|
+
/**
|
|
117
|
+
* When true, the active track segment grows from the center (50%)
|
|
118
|
+
* outward toward the thumb position.
|
|
119
|
+
* @default false
|
|
120
|
+
*/
|
|
121
|
+
isCentered?: boolean;
|
|
122
|
+
/** Disables all interaction. @default false */
|
|
123
|
+
disabled?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* When true, shows a floating value indicator tooltip above the thumb.
|
|
126
|
+
* @default false
|
|
127
|
+
*/
|
|
128
|
+
showValueIndicator?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* When true, shows tick marks along the track.
|
|
131
|
+
* Only applicable if `step` > 0.
|
|
132
|
+
* @default false
|
|
133
|
+
*/
|
|
134
|
+
showTicks?: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Track shape configuration for border radius.
|
|
137
|
+
* - "md3": Default MD3 specific border radius per size
|
|
138
|
+
* - "full": Fully rounded ends (pill shape - size/2)
|
|
139
|
+
* - number: Custom border radius in px
|
|
140
|
+
* @default "md3"
|
|
141
|
+
*/
|
|
142
|
+
trackShape?: SliderTrackShape;
|
|
143
|
+
/**
|
|
144
|
+
* Icon rendered inside the track (inset icon).
|
|
145
|
+
* MD3 spec: only valid for M, L, XL track sizes.
|
|
146
|
+
* The icon moves from the active track to the inactive track
|
|
147
|
+
* when there's not enough space at low values.
|
|
148
|
+
* Do not use with `isCentered` or `RangeSlider`.
|
|
149
|
+
*/
|
|
150
|
+
insetIcon?: React.ReactNode;
|
|
151
|
+
/**
|
|
152
|
+
* Alternate icon shown when value equals `min`.
|
|
153
|
+
* Swaps with `insetIcon` at the minimum value
|
|
154
|
+
* (e.g., a mute icon replacing a volume icon when volume = 0).
|
|
155
|
+
*/
|
|
156
|
+
insetIconAtMin?: React.ReactNode;
|
|
157
|
+
/**
|
|
158
|
+
* Icon rendered inside the track at the trailing end (right side).
|
|
159
|
+
* Only valid for track sizes >= 40dp (e.g. XL).
|
|
160
|
+
*/
|
|
161
|
+
insetIconTrailing?: React.ReactNode;
|
|
162
|
+
/**
|
|
163
|
+
* Alternate icon shown when value equals `max`.
|
|
164
|
+
* Swaps with `insetIconTrailing` at the maximum value.
|
|
165
|
+
*/
|
|
166
|
+
insetIconAtMax?: React.ReactNode;
|
|
167
|
+
/** Additional CSS class applied to the outermost wrapper. */
|
|
168
|
+
className?: string;
|
|
169
|
+
/**
|
|
170
|
+
* Accessible label for the slider when no visible label exists.
|
|
171
|
+
* Required if parent does not have a visible label.
|
|
172
|
+
*/
|
|
173
|
+
"aria-label"?: string;
|
|
174
|
+
/** ID of a visible label element. Required if `aria-label` is not provided. */
|
|
175
|
+
"aria-labelledby"?: string;
|
|
176
|
+
/**
|
|
177
|
+
* Format function for the displayed value in the value indicator tooltip.
|
|
178
|
+
* Defaults to `String(value)`.
|
|
179
|
+
*/
|
|
180
|
+
formatValue?: (value: number) => string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Range Slider Props ──────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Props for the `<RangeSlider>` component.
|
|
187
|
+
*
|
|
188
|
+
* Extends `SliderProps` with tuple-based value API.
|
|
189
|
+
* The two thumbs cannot cross each other.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```tsx
|
|
193
|
+
* <RangeSlider
|
|
194
|
+
* value={[20, 80]}
|
|
195
|
+
* onValueChange={([start, end]) => setRange([start, end])}
|
|
196
|
+
* />
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
export interface RangeSliderProps
|
|
200
|
+
extends Omit<
|
|
201
|
+
SliderProps,
|
|
202
|
+
| "value"
|
|
203
|
+
| "defaultValue"
|
|
204
|
+
| "onValueChange"
|
|
205
|
+
| "onValueChangeEnd"
|
|
206
|
+
| "isCentered"
|
|
207
|
+
> {
|
|
208
|
+
/** Controlled [start, end] tuple. Use with `onValueChange`. */
|
|
209
|
+
value?: [number, number];
|
|
210
|
+
/** Initial [start, end] tuple for uncontrolled usage. */
|
|
211
|
+
defaultValue?: [number, number];
|
|
212
|
+
/** Called whenever [start, end] changes during interaction. */
|
|
213
|
+
onValueChange?: (value: [number, number]) => void;
|
|
214
|
+
/** Called when the user finishes dragging either thumb. */
|
|
215
|
+
onValueChangeEnd?: (value: [number, number]) => void;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─── Internal Sub-component Props ────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Props for `<SliderTrack>`.
|
|
222
|
+
* @internal
|
|
223
|
+
*/
|
|
224
|
+
export interface SliderTrackProps {
|
|
225
|
+
/** Current thumb position as 0–1 fraction. */
|
|
226
|
+
percent: number;
|
|
227
|
+
trackSize: SliderTrackSize;
|
|
228
|
+
orientation: SliderOrientation;
|
|
229
|
+
variant: SliderVariant;
|
|
230
|
+
isCentered: boolean;
|
|
231
|
+
/** For discrete mode: step size. 0 = no ticks. */
|
|
232
|
+
step: number;
|
|
233
|
+
min: number;
|
|
234
|
+
max: number;
|
|
235
|
+
disabled: boolean;
|
|
236
|
+
trackShape?: SliderTrackShape;
|
|
237
|
+
/** Ref forwarded to the root track element for drag constraint. */
|
|
238
|
+
trackRef: React.RefObject<HTMLDivElement | null>;
|
|
239
|
+
/** onClick handler on the track for click-to-jump. */
|
|
240
|
+
onTrackPointerDown?: (e: React.PointerEvent<HTMLDivElement>) => void;
|
|
241
|
+
/**
|
|
242
|
+
* Icon rendered inside the track (inset icon).
|
|
243
|
+
* @internal — passed down from Slider after guard check.
|
|
244
|
+
*/
|
|
245
|
+
insetIcon?: React.ReactNode;
|
|
246
|
+
/** Alternate icon swapped in when value === min. @internal */
|
|
247
|
+
insetIconAtMin?: React.ReactNode;
|
|
248
|
+
/** Icon rendered at the trailing end (right side). @internal */
|
|
249
|
+
insetIconTrailing?: React.ReactNode;
|
|
250
|
+
/** Alternate icon swapped in when value === max. @internal */
|
|
251
|
+
insetIconAtMax?: React.ReactNode;
|
|
252
|
+
/** Current slider value — used for inset icon swap at min/max. @internal */
|
|
253
|
+
value?: number;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Props for `<SliderThumb>`.
|
|
258
|
+
* @internal
|
|
259
|
+
*/
|
|
260
|
+
export interface SliderThumbProps {
|
|
261
|
+
/** Current value (for ARIA). */
|
|
262
|
+
value: number;
|
|
263
|
+
/** Current 0–1 fraction (for positioning). */
|
|
264
|
+
percent: number;
|
|
265
|
+
min: number;
|
|
266
|
+
max: number;
|
|
267
|
+
step: number;
|
|
268
|
+
disabled: boolean;
|
|
269
|
+
orientation: SliderOrientation;
|
|
270
|
+
showValueIndicator: boolean;
|
|
271
|
+
/** For the drag constraint ref. */
|
|
272
|
+
trackRef: React.RefObject<HTMLDivElement | null>;
|
|
273
|
+
trackSize: SliderTrackSize;
|
|
274
|
+
variant: SliderVariant;
|
|
275
|
+
/** Called during pointer drag with new value. */
|
|
276
|
+
onValueChange: (value: number) => void;
|
|
277
|
+
/** Called on drag end / keyboard commit. */
|
|
278
|
+
onValueChangeEnd?: (value: number) => void;
|
|
279
|
+
/** Value display formatter. */
|
|
280
|
+
formatValue?: (value: number) => string;
|
|
281
|
+
/** Unique ID for ARIA. */
|
|
282
|
+
thumbId?: string;
|
|
283
|
+
/** zIndex for RangeSlider layering. */
|
|
284
|
+
zIndex?: number;
|
|
285
|
+
/** Optional accessible label for this specific thumb. */
|
|
286
|
+
"aria-label"?: string;
|
|
287
|
+
"aria-labelledby"?: string;
|
|
288
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file snackbar/index.ts
|
|
3
|
+
* Barrel re-export for the MD3 Expressive Snackbar component system.
|
|
4
|
+
*/
|
|
5
|
+
export type {
|
|
6
|
+
SnackbarData,
|
|
7
|
+
SnackbarDuration,
|
|
8
|
+
SnackbarHostProps,
|
|
9
|
+
SnackbarProps,
|
|
10
|
+
SnackbarResult,
|
|
11
|
+
SnackbarVisuals,
|
|
12
|
+
UseSnackbarStateReturn,
|
|
13
|
+
} from "./snackbar";
|
|
14
|
+
export {
|
|
15
|
+
Snackbar,
|
|
16
|
+
SnackbarHost,
|
|
17
|
+
SnackbarProvider,
|
|
18
|
+
useSnackbar,
|
|
19
|
+
useSnackbarState,
|
|
20
|
+
} from "./snackbar";
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import type { SnackbarData } from "./snackbar";
|
|
7
|
+
import {
|
|
8
|
+
Snackbar,
|
|
9
|
+
SnackbarHost,
|
|
10
|
+
SnackbarProvider,
|
|
11
|
+
useSnackbar,
|
|
12
|
+
useSnackbarState,
|
|
13
|
+
} from "./snackbar";
|
|
14
|
+
|
|
15
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** Builds a minimal SnackbarData object for pure Snackbar tests. */
|
|
18
|
+
function makeSnackbarData(overrides: Partial<SnackbarData> = {}): SnackbarData {
|
|
19
|
+
return {
|
|
20
|
+
id: "test-id",
|
|
21
|
+
visuals: { message: "Test message", duration: 9999999 },
|
|
22
|
+
resolve: vi.fn(),
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── 1. Renders message ───────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe("Snackbar — renders message", () => {
|
|
30
|
+
it("displays the message text", () => {
|
|
31
|
+
const data = makeSnackbarData({
|
|
32
|
+
visuals: { message: "File deleted", duration: 9999999 },
|
|
33
|
+
});
|
|
34
|
+
render(<Snackbar data={data} />);
|
|
35
|
+
expect(screen.getByText("File deleted")).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ─── 2. Renders action button ─────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe("Snackbar — action button", () => {
|
|
42
|
+
it("renders action button when actionLabel is provided", () => {
|
|
43
|
+
const data = makeSnackbarData({
|
|
44
|
+
visuals: { message: "Archived", actionLabel: "Undo", duration: 9999999 },
|
|
45
|
+
});
|
|
46
|
+
render(<Snackbar data={data} />);
|
|
47
|
+
expect(screen.getByRole("button", { name: "Undo" })).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("does not render action button when actionLabel is absent", () => {
|
|
51
|
+
const data = makeSnackbarData({
|
|
52
|
+
visuals: { message: "No action", duration: 9999999 },
|
|
53
|
+
});
|
|
54
|
+
render(<Snackbar data={data} />);
|
|
55
|
+
// Only the close button might be present — but not an "action"
|
|
56
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ─── 3. Action click resolves 'action-performed' ──────────────────────────────
|
|
61
|
+
|
|
62
|
+
describe("Snackbar — action click resolves promise", () => {
|
|
63
|
+
it("calls resolve with 'action-performed' when action button is clicked", async () => {
|
|
64
|
+
const user = userEvent.setup();
|
|
65
|
+
const resolveMock = vi.fn();
|
|
66
|
+
const data = makeSnackbarData({
|
|
67
|
+
visuals: { message: "Saved", actionLabel: "Undo", duration: 9999999 },
|
|
68
|
+
resolve: resolveMock,
|
|
69
|
+
});
|
|
70
|
+
render(<Snackbar data={data} />);
|
|
71
|
+
await user.click(screen.getByRole("button", { name: "Undo" }));
|
|
72
|
+
expect(resolveMock).toHaveBeenCalledWith("action-performed");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── 4. Auto-dismiss ─────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe("Snackbar — auto-dismiss", () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
vi.useFakeTimers();
|
|
81
|
+
});
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
vi.useRealTimers();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("calls resolve('dismissed') after short duration (4000ms)", () => {
|
|
87
|
+
const resolveMock = vi.fn();
|
|
88
|
+
const data = makeSnackbarData({
|
|
89
|
+
visuals: { message: "Auto dismiss", duration: "short" },
|
|
90
|
+
resolve: resolveMock,
|
|
91
|
+
});
|
|
92
|
+
render(<Snackbar data={data} />);
|
|
93
|
+
expect(resolveMock).not.toHaveBeenCalled();
|
|
94
|
+
act(() => {
|
|
95
|
+
vi.advanceTimersByTime(4000);
|
|
96
|
+
});
|
|
97
|
+
expect(resolveMock).toHaveBeenCalledWith("dismissed");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("calls resolve after custom duration", () => {
|
|
101
|
+
const resolveMock = vi.fn();
|
|
102
|
+
const data = makeSnackbarData({
|
|
103
|
+
visuals: { message: "Custom timer", duration: 1500 },
|
|
104
|
+
resolve: resolveMock,
|
|
105
|
+
});
|
|
106
|
+
render(<Snackbar data={data} />);
|
|
107
|
+
act(() => {
|
|
108
|
+
vi.advanceTimersByTime(1499);
|
|
109
|
+
});
|
|
110
|
+
expect(resolveMock).not.toHaveBeenCalled();
|
|
111
|
+
act(() => {
|
|
112
|
+
vi.advanceTimersByTime(1);
|
|
113
|
+
});
|
|
114
|
+
expect(resolveMock).toHaveBeenCalledWith("dismissed");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ─── 5. Dismiss button ────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
describe("Snackbar — dismiss button", () => {
|
|
121
|
+
it("renders close button when withDismissAction=true", () => {
|
|
122
|
+
const data = makeSnackbarData({
|
|
123
|
+
visuals: {
|
|
124
|
+
message: "Hello",
|
|
125
|
+
withDismissAction: true,
|
|
126
|
+
duration: 9999999,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
render(<Snackbar data={data} />);
|
|
130
|
+
expect(
|
|
131
|
+
screen.getByRole("button", { name: /dismiss/i }),
|
|
132
|
+
).toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("calls resolve('dismissed') when close button is clicked", async () => {
|
|
136
|
+
const user = userEvent.setup();
|
|
137
|
+
const resolveMock = vi.fn();
|
|
138
|
+
const data = makeSnackbarData({
|
|
139
|
+
visuals: {
|
|
140
|
+
message: "Hello",
|
|
141
|
+
withDismissAction: true,
|
|
142
|
+
duration: 9999999,
|
|
143
|
+
},
|
|
144
|
+
resolve: resolveMock,
|
|
145
|
+
});
|
|
146
|
+
render(<Snackbar data={data} />);
|
|
147
|
+
await user.click(screen.getByRole("button", { name: /dismiss/i }));
|
|
148
|
+
expect(resolveMock).toHaveBeenCalledWith("dismissed");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ─── 6. Queue behavior ────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
describe("SnackbarHost — queue behavior", () => {
|
|
155
|
+
it("shows second snackbar after first is dismissed", async () => {
|
|
156
|
+
const user = userEvent.setup();
|
|
157
|
+
|
|
158
|
+
function Wrapper() {
|
|
159
|
+
const state = useSnackbarState();
|
|
160
|
+
return (
|
|
161
|
+
<>
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
onClick={() =>
|
|
165
|
+
state.showSnackbar({
|
|
166
|
+
message: "Snackbar One",
|
|
167
|
+
actionLabel: "Close One",
|
|
168
|
+
duration: 9999999,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
>
|
|
172
|
+
Show One
|
|
173
|
+
</button>
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
onClick={() =>
|
|
177
|
+
state.showSnackbar({
|
|
178
|
+
message: "Snackbar Two",
|
|
179
|
+
duration: 9999999,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
>
|
|
183
|
+
Show Two
|
|
184
|
+
</button>
|
|
185
|
+
<SnackbarHost state={state} />
|
|
186
|
+
</>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
render(<Wrapper />);
|
|
191
|
+
|
|
192
|
+
// Show first
|
|
193
|
+
await user.click(screen.getByRole("button", { name: "Show One" }));
|
|
194
|
+
expect(screen.getByText("Snackbar One")).toBeInTheDocument();
|
|
195
|
+
|
|
196
|
+
// Queue second immediately (first still showing)
|
|
197
|
+
await user.click(screen.getByRole("button", { name: "Show Two" }));
|
|
198
|
+
|
|
199
|
+
// Second should NOT be visible yet
|
|
200
|
+
expect(screen.queryByText("Snackbar Two")).not.toBeInTheDocument();
|
|
201
|
+
|
|
202
|
+
// Dismiss first via its action button
|
|
203
|
+
await user.click(screen.getByRole("button", { name: "Close One" }));
|
|
204
|
+
|
|
205
|
+
// Second should now appear
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(screen.getByText("Snackbar Two")).toBeInTheDocument();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ─── 7. actionOnNewLine layout ────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
describe("Snackbar — actionOnNewLine", () => {
|
|
215
|
+
it("applies flex-col when actionOnNewLine=true", () => {
|
|
216
|
+
const data = makeSnackbarData({
|
|
217
|
+
visuals: {
|
|
218
|
+
message: "Long message text here",
|
|
219
|
+
actionLabel: "Action",
|
|
220
|
+
actionOnNewLine: true,
|
|
221
|
+
duration: 9999999,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
const { container } = render(<Snackbar data={data} />);
|
|
225
|
+
// The snackbar root div should have flex-col
|
|
226
|
+
const snackbarEl = container.firstChild as HTMLElement;
|
|
227
|
+
expect(snackbarEl.className).toContain("flex-col");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("applies flex-row by default (actionOnNewLine=false)", () => {
|
|
231
|
+
const data = makeSnackbarData({
|
|
232
|
+
visuals: {
|
|
233
|
+
message: "Message",
|
|
234
|
+
actionLabel: "Action",
|
|
235
|
+
actionOnNewLine: false,
|
|
236
|
+
duration: 9999999,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
const { container } = render(<Snackbar data={data} />);
|
|
240
|
+
const snackbarEl = container.firstChild as HTMLElement;
|
|
241
|
+
expect(snackbarEl.className).toContain("flex-row");
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ─── 8. Accessibility ─────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
describe("Snackbar — accessibility", () => {
|
|
248
|
+
it("has role='status' on the container", () => {
|
|
249
|
+
const data = makeSnackbarData();
|
|
250
|
+
render(<Snackbar data={data} />);
|
|
251
|
+
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("has aria-live='polite' on the container", () => {
|
|
255
|
+
const data = makeSnackbarData();
|
|
256
|
+
const { container } = render(<Snackbar data={data} />);
|
|
257
|
+
const statusEl = container.querySelector("[role='status']");
|
|
258
|
+
expect(statusEl).toHaveAttribute("aria-live", "polite");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("has aria-atomic='true' on the container", () => {
|
|
262
|
+
const data = makeSnackbarData();
|
|
263
|
+
const { container } = render(<Snackbar data={data} />);
|
|
264
|
+
const statusEl = container.querySelector("[role='status']");
|
|
265
|
+
expect(statusEl).toHaveAttribute("aria-atomic", "true");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ─── 9. Custom className ──────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
describe("Snackbar — custom className", () => {
|
|
272
|
+
it("applies custom className to the container", () => {
|
|
273
|
+
const data = makeSnackbarData({
|
|
274
|
+
visuals: {
|
|
275
|
+
message: "Styled",
|
|
276
|
+
className: "my-custom-class",
|
|
277
|
+
duration: 9999999,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
const { container } = render(<Snackbar data={data} />);
|
|
281
|
+
const snackbarEl = container.firstChild as HTMLElement;
|
|
282
|
+
expect(snackbarEl.className).toContain("my-custom-class");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ─── 10. SnackbarProvider + useSnackbar ───────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
describe("SnackbarProvider + useSnackbar", () => {
|
|
289
|
+
it("provides showSnackbar via useSnackbar hook", async () => {
|
|
290
|
+
const user = userEvent.setup();
|
|
291
|
+
function TestConsumer() {
|
|
292
|
+
const { showSnackbar } = useSnackbar();
|
|
293
|
+
return (
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
onClick={async () => {
|
|
297
|
+
await showSnackbar({
|
|
298
|
+
message: "Provider test",
|
|
299
|
+
duration: 9999999,
|
|
300
|
+
});
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
Trigger
|
|
304
|
+
</button>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
render(
|
|
309
|
+
<SnackbarProvider>
|
|
310
|
+
<TestConsumer />
|
|
311
|
+
</SnackbarProvider>,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
await user.click(screen.getByRole("button", { name: "Trigger" }));
|
|
315
|
+
|
|
316
|
+
// Snackbar should appear in the provider's SnackbarHost
|
|
317
|
+
await waitFor(() => {
|
|
318
|
+
expect(screen.getByText("Provider test")).toBeInTheDocument();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("throws when useSnackbar is used outside SnackbarProvider", () => {
|
|
323
|
+
const originalError = console.error;
|
|
324
|
+
// Suppress React's boundary error log
|
|
325
|
+
console.error = vi.fn();
|
|
326
|
+
|
|
327
|
+
function BadConsumer() {
|
|
328
|
+
useSnackbar();
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
expect(() => render(<BadConsumer />)).toThrow(
|
|
333
|
+
"useSnackbar must be used within a <SnackbarProvider>.",
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
console.error = originalError;
|
|
337
|
+
});
|
|
338
|
+
});
|