@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,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file dialog.tsx
|
|
3
|
+
*
|
|
4
|
+
* Sub-system Component Dialog theo phong cách hiển thị MD3 Expressive.
|
|
5
|
+
*
|
|
6
|
+
* Được kế thừa trên nền lõi của hệ mã Radix UI Dialog primitives đi kèm gói kén Framer Motion dùng vào việc bổ sung
|
|
7
|
+
* nhip điệu đẩy Spring ở hướng hiện vào (Entrance) cũng như bay lên (Exit); ăn liền với bản thiết kế specs MD3 Expressive siêu quyến rũ.
|
|
8
|
+
* Phục vụ cả ở chế độ Standard (Tiêu chuẩn lọt thỏm) và phiên bản Full-Screen tràn viền toàn diện.
|
|
9
|
+
*
|
|
10
|
+
* @see https://m3.material.io/components/dialogs/overview
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as RadixDialog from "@radix-ui/react-dialog";
|
|
14
|
+
import { AnimatePresence, m } from "motion/react";
|
|
15
|
+
import * as React from "react";
|
|
16
|
+
import { cn } from "../lib/utils";
|
|
17
|
+
import { Icon } from "./icon";
|
|
18
|
+
import { IconButton } from "./icon-button";
|
|
19
|
+
import { ScrollArea } from "./scroll-area";
|
|
20
|
+
|
|
21
|
+
// ─── MD3 Spring Config (Expressive) ──────────────────────────────────────────
|
|
22
|
+
const MD3_SPRING = {
|
|
23
|
+
type: "spring" as const,
|
|
24
|
+
stiffness: 400,
|
|
25
|
+
damping: 30,
|
|
26
|
+
mass: 1,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const MD3_OVERLAY_ANIM = {
|
|
30
|
+
initial: { opacity: 0 },
|
|
31
|
+
animate: {
|
|
32
|
+
opacity: 1,
|
|
33
|
+
transition: { duration: 0.2, ease: "easeOut" as const },
|
|
34
|
+
},
|
|
35
|
+
exit: { opacity: 0, transition: { duration: 0.15, ease: "easeIn" as const } },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const MD3_CONTENT_ANIM = {
|
|
39
|
+
initial: { opacity: 0, scale: 0.85, y: 24 },
|
|
40
|
+
animate: { opacity: 1, scale: 1, y: 0, transition: MD3_SPRING },
|
|
41
|
+
exit: {
|
|
42
|
+
opacity: 0,
|
|
43
|
+
scale: 0.95,
|
|
44
|
+
y: 8,
|
|
45
|
+
transition: { duration: 0.15, ease: "easeIn" as const },
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const MD3_FULLSCREEN_ANIM = {
|
|
50
|
+
initial: { y: "100%" },
|
|
51
|
+
animate: { y: 0, transition: MD3_SPRING },
|
|
52
|
+
exit: { y: "100%", transition: { duration: 0.2, ease: "easeIn" as const } },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Thuộc tính của cội gốc Root `Dialog`. Chức năng làm gương phản hồi của Radix `Dialog.Root` qua dạng controlled state đóng hay mở.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* const [open, setOpen] = React.useState(false);
|
|
63
|
+
*
|
|
64
|
+
* <Dialog open={open} onOpenChange={setOpen}>
|
|
65
|
+
* <DialogTrigger asChild>
|
|
66
|
+
* <Button>Bấm Mở Dialog</Button>
|
|
67
|
+
* </DialogTrigger>
|
|
68
|
+
* <DialogContent>
|
|
69
|
+
* <DialogHeader>
|
|
70
|
+
* <DialogTitle>Bạn có muốn phiêu lưu không?</DialogTitle>
|
|
71
|
+
* </DialogHeader>
|
|
72
|
+
* <DialogBody>Chuẩn bị lên đồ rời khỏi hang nào.</DialogBody>
|
|
73
|
+
* <DialogFooter>
|
|
74
|
+
* <Button onClick={() => setOpen(false)} variant="text">Đóng</Button>
|
|
75
|
+
* </DialogFooter>
|
|
76
|
+
* </DialogContent>
|
|
77
|
+
* </Dialog>
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export interface DialogProps {
|
|
81
|
+
/** Whether the dialog is open (controlled). Omit for uncontrolled. */
|
|
82
|
+
open?: boolean;
|
|
83
|
+
/** Called when the open state should change. */
|
|
84
|
+
onOpenChange?: (open: boolean) => void;
|
|
85
|
+
/** Dialog trigger + content. */
|
|
86
|
+
children: React.ReactNode;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Các props được tiêm vào component bao ngoài Container `DialogContent` thuộc dạng Normal Standard.
|
|
91
|
+
*
|
|
92
|
+
* @see {@link DialogContent}
|
|
93
|
+
*/
|
|
94
|
+
export interface DialogContentProps
|
|
95
|
+
extends React.ComponentPropsWithoutRef<typeof RadixDialog.Content> {
|
|
96
|
+
/** Vô hình đi đi nút Close dấu (X) góc phải trên. @default false */
|
|
97
|
+
hideCloseButton?: boolean;
|
|
98
|
+
className?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Thuộc tính Props của biến thể `DialogFullScreenContent` chuyên dụng dành riêng cho Mode Full-Screen tràn màn hình.
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* Những hộp thoại Full-screen có biệt tài tự nới rộng và xâm chiếm cả bề ngang dọc nguyên thiết bị. Nó còn thiết lập một đường Top App bar (thanh ngang đỉnh)
|
|
106
|
+
* kẹp chung cả 1 nhãn title mô tả đỉnh, một nút icon X dẹp ở rìa, thêm luôn hẳn cái nút Confirm cực xịn xò.
|
|
107
|
+
*
|
|
108
|
+
* @see {@link DialogFullScreenContent}
|
|
109
|
+
* @see https://m3.material.io/components/dialogs/guidelines#full-screen
|
|
110
|
+
*/
|
|
111
|
+
export interface DialogFullScreenContentProps
|
|
112
|
+
extends React.ComponentPropsWithoutRef<typeof RadixDialog.Content> {
|
|
113
|
+
/** Nhãn Title nằm vùng khu vực thanh ngang Top Bar. */
|
|
114
|
+
title?: string;
|
|
115
|
+
/** Chữ viết đính kèm bên trong cục Nút nhấn thao tác ngay trên góc Top App bar đó (VD: "Lưu lại", "Save"). */
|
|
116
|
+
actionLabel?: string;
|
|
117
|
+
/** Hàm handler phát động cờ để kích chạy tính năng lưu, xác nhận kia. */
|
|
118
|
+
onAction?: () => void;
|
|
119
|
+
/** Rạch một làn kẻ chia cách thân body nội dung cuộn bên dưới và dòng App bar cố thủ bên trên. @default false */
|
|
120
|
+
showDivider?: boolean;
|
|
121
|
+
className?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Re-exports wrapper ───────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Gốc rễ Root của Component Dialog — trạc lấp lên cái module `Dialog.Root` của nhà Radix.
|
|
128
|
+
*
|
|
129
|
+
* @remarks Cơ năng hoạt động ở chế độ Controlled (kiểm soát vòng đời ở Client) xài mảng `open`/`onOpenChange`.
|
|
130
|
+
* Tuy thế bạn nếu không thích thì đừng có xài truyền mấy cái Props tay đôi trên, mà bỏ xài kiểu Untracked thông qua cái cục `DialogTrigger` là đủ.
|
|
131
|
+
*/
|
|
132
|
+
const Dialog = ({ open, onOpenChange, children }: DialogProps) => (
|
|
133
|
+
<RadixDialog.Root open={open} onOpenChange={onOpenChange}>
|
|
134
|
+
{children}
|
|
135
|
+
</RadixDialog.Root>
|
|
136
|
+
);
|
|
137
|
+
Dialog.displayName = "Dialog";
|
|
138
|
+
|
|
139
|
+
/** Bộ Trigger gieo phát đà giúp tắt bật cờ State đóng mở của Dialog con, dùng kèm kĩ năng nhét thông qua cái cầu `asChild`. */
|
|
140
|
+
const DialogTrigger = RadixDialog.Trigger;
|
|
141
|
+
DialogTrigger.displayName = "DialogTrigger";
|
|
142
|
+
|
|
143
|
+
// ─── Portal + Overlay + Content ───────────────────────────────────────────────
|
|
144
|
+
const DialogPortal = ({
|
|
145
|
+
open,
|
|
146
|
+
children,
|
|
147
|
+
}: {
|
|
148
|
+
open?: boolean;
|
|
149
|
+
children: React.ReactNode;
|
|
150
|
+
}) => (
|
|
151
|
+
<RadixDialog.Portal forceMount>
|
|
152
|
+
<AnimatePresence>
|
|
153
|
+
{open ? React.Children.toArray(children) : null}
|
|
154
|
+
</AnimatePresence>
|
|
155
|
+
</RadixDialog.Portal>
|
|
156
|
+
);
|
|
157
|
+
DialogPortal.displayName = "DialogPortal";
|
|
158
|
+
|
|
159
|
+
const DialogOverlay = React.forwardRef<
|
|
160
|
+
React.ComponentRef<typeof RadixDialog.Overlay>,
|
|
161
|
+
React.ComponentPropsWithoutRef<typeof RadixDialog.Overlay>
|
|
162
|
+
>(({ className, ...props }, ref) => (
|
|
163
|
+
<RadixDialog.Overlay ref={ref} asChild {...props}>
|
|
164
|
+
<m.div
|
|
165
|
+
className={cn("fixed inset-0 z-50 bg-black/32", className)}
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
{...MD3_OVERLAY_ANIM}
|
|
168
|
+
/>
|
|
169
|
+
</RadixDialog.Overlay>
|
|
170
|
+
));
|
|
171
|
+
DialogOverlay.displayName = "DialogOverlay";
|
|
172
|
+
|
|
173
|
+
const DialogContent = React.forwardRef<
|
|
174
|
+
React.ComponentRef<typeof RadixDialog.Content>,
|
|
175
|
+
DialogContentProps
|
|
176
|
+
>(({ className, children, hideCloseButton = false, ...props }, ref) => (
|
|
177
|
+
<RadixDialog.Content
|
|
178
|
+
ref={ref}
|
|
179
|
+
asChild
|
|
180
|
+
aria-describedby={undefined}
|
|
181
|
+
{...props}
|
|
182
|
+
>
|
|
183
|
+
<m.div
|
|
184
|
+
className={cn(
|
|
185
|
+
"fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
|
|
186
|
+
"w-[calc(100%-2rem)] max-w-140",
|
|
187
|
+
"rounded-[28px] bg-m3-surface-container-high p-6",
|
|
188
|
+
"shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-m3-primary",
|
|
189
|
+
className,
|
|
190
|
+
)}
|
|
191
|
+
role="dialog"
|
|
192
|
+
{...MD3_CONTENT_ANIM}
|
|
193
|
+
>
|
|
194
|
+
{children}
|
|
195
|
+
{!hideCloseButton && (
|
|
196
|
+
<RadixDialog.Close asChild aria-label="Close dialog">
|
|
197
|
+
<IconButton
|
|
198
|
+
size="sm"
|
|
199
|
+
colorStyle="filled"
|
|
200
|
+
className="absolute right-4 top-4"
|
|
201
|
+
aria-label="Close"
|
|
202
|
+
>
|
|
203
|
+
<Icon name="close" aria-hidden="true" />
|
|
204
|
+
</IconButton>
|
|
205
|
+
</RadixDialog.Close>
|
|
206
|
+
)}
|
|
207
|
+
</m.div>
|
|
208
|
+
</RadixDialog.Content>
|
|
209
|
+
));
|
|
210
|
+
DialogContent.displayName = "DialogContent";
|
|
211
|
+
|
|
212
|
+
// ─── Header & Related ────────────────────────────────────────────────────────
|
|
213
|
+
const DialogIcon = React.forwardRef<
|
|
214
|
+
HTMLDivElement,
|
|
215
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
216
|
+
>(({ className, children, ...props }, ref) => (
|
|
217
|
+
<div
|
|
218
|
+
ref={ref}
|
|
219
|
+
className={cn("flex justify-center mb-4 text-m3-secondary", className)}
|
|
220
|
+
aria-hidden="true"
|
|
221
|
+
{...props}
|
|
222
|
+
>
|
|
223
|
+
{children}
|
|
224
|
+
</div>
|
|
225
|
+
));
|
|
226
|
+
DialogIcon.displayName = "DialogIcon";
|
|
227
|
+
|
|
228
|
+
const DialogHeader = ({
|
|
229
|
+
className,
|
|
230
|
+
...props
|
|
231
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
232
|
+
<div className={cn("flex flex-col gap-2 mb-4", className)} {...props} />
|
|
233
|
+
);
|
|
234
|
+
DialogHeader.displayName = "DialogHeader";
|
|
235
|
+
|
|
236
|
+
// RadixDialog.Title surfaces to the accessibility tree — required
|
|
237
|
+
const DialogTitle = React.forwardRef<
|
|
238
|
+
React.ComponentRef<typeof RadixDialog.Title>,
|
|
239
|
+
React.ComponentPropsWithoutRef<typeof RadixDialog.Title>
|
|
240
|
+
>(({ className, asChild, ...props }, ref) => (
|
|
241
|
+
<RadixDialog.Title
|
|
242
|
+
ref={ref}
|
|
243
|
+
asChild={asChild}
|
|
244
|
+
className={cn(
|
|
245
|
+
"text-[24px] leading-8 font-normal text-m3-on-surface tracking-[0em]",
|
|
246
|
+
className,
|
|
247
|
+
)}
|
|
248
|
+
{...props}
|
|
249
|
+
/>
|
|
250
|
+
));
|
|
251
|
+
DialogTitle.displayName = "DialogTitle";
|
|
252
|
+
|
|
253
|
+
const DialogDescription = React.forwardRef<
|
|
254
|
+
React.ComponentRef<typeof RadixDialog.Description>,
|
|
255
|
+
React.ComponentPropsWithoutRef<typeof RadixDialog.Description>
|
|
256
|
+
>(({ className, asChild, ...props }, ref) => (
|
|
257
|
+
<RadixDialog.Description
|
|
258
|
+
ref={ref}
|
|
259
|
+
asChild={asChild}
|
|
260
|
+
className={cn("text-sm text-m3-on-surface-variant leading-5", className)}
|
|
261
|
+
{...props}
|
|
262
|
+
/>
|
|
263
|
+
));
|
|
264
|
+
DialogDescription.displayName = "DialogDescription";
|
|
265
|
+
|
|
266
|
+
// ─── Body & Footer ───────────────────────────────────────────────────────────
|
|
267
|
+
const DialogBody = React.forwardRef<
|
|
268
|
+
HTMLDivElement,
|
|
269
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
270
|
+
>(({ className, children, dir, ...props }, ref) => (
|
|
271
|
+
<ScrollArea
|
|
272
|
+
ref={ref}
|
|
273
|
+
type="hover"
|
|
274
|
+
dir={dir as "ltr" | "rtl" | undefined}
|
|
275
|
+
className={cn("max-h-[calc(85dvh-200px)] -mx-6", className)}
|
|
276
|
+
viewportClassName="px-6"
|
|
277
|
+
{...props}
|
|
278
|
+
>
|
|
279
|
+
{children}
|
|
280
|
+
</ScrollArea>
|
|
281
|
+
));
|
|
282
|
+
DialogBody.displayName = "DialogBody";
|
|
283
|
+
|
|
284
|
+
const DialogFooter = ({
|
|
285
|
+
className,
|
|
286
|
+
...props
|
|
287
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
288
|
+
<div
|
|
289
|
+
className={cn("flex flex-row justify-end gap-2 mt-6", className)}
|
|
290
|
+
{...props}
|
|
291
|
+
/>
|
|
292
|
+
);
|
|
293
|
+
DialogFooter.displayName = "DialogFooter";
|
|
294
|
+
|
|
295
|
+
const DialogClose = RadixDialog.Close;
|
|
296
|
+
|
|
297
|
+
// ─── Full Screen Content Variant ──────────────────────────────────────────────
|
|
298
|
+
const DialogFullScreenContent = React.forwardRef<
|
|
299
|
+
React.ComponentRef<typeof RadixDialog.Content>,
|
|
300
|
+
DialogFullScreenContentProps
|
|
301
|
+
>(
|
|
302
|
+
(
|
|
303
|
+
{
|
|
304
|
+
className,
|
|
305
|
+
children,
|
|
306
|
+
title,
|
|
307
|
+
actionLabel,
|
|
308
|
+
onAction,
|
|
309
|
+
showDivider,
|
|
310
|
+
...props
|
|
311
|
+
},
|
|
312
|
+
ref,
|
|
313
|
+
) => (
|
|
314
|
+
<RadixDialog.Content
|
|
315
|
+
ref={ref}
|
|
316
|
+
asChild
|
|
317
|
+
aria-describedby={undefined}
|
|
318
|
+
{...props}
|
|
319
|
+
>
|
|
320
|
+
<m.div
|
|
321
|
+
className={cn(
|
|
322
|
+
"fixed inset-0 z-50 w-full h-full bg-m3-surface flex flex-col",
|
|
323
|
+
"outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-m3-primary",
|
|
324
|
+
className,
|
|
325
|
+
)}
|
|
326
|
+
role="dialog"
|
|
327
|
+
{...MD3_FULLSCREEN_ANIM}
|
|
328
|
+
>
|
|
329
|
+
<div className="flex shrink-0 items-center px-4 h-14 gap-2 bg-m3-surface">
|
|
330
|
+
<RadixDialog.Close asChild aria-label="Close dialog">
|
|
331
|
+
<IconButton size="sm" colorStyle="filled" aria-label="Close">
|
|
332
|
+
<Icon name="close" aria-hidden="true" />
|
|
333
|
+
</IconButton>
|
|
334
|
+
</RadixDialog.Close>
|
|
335
|
+
|
|
336
|
+
{title && (
|
|
337
|
+
<DialogTitle className="flex-1 text-[22px] leading-7 font-medium truncate pr-2">
|
|
338
|
+
{title}
|
|
339
|
+
</DialogTitle>
|
|
340
|
+
)}
|
|
341
|
+
|
|
342
|
+
{actionLabel && onAction && (
|
|
343
|
+
<button
|
|
344
|
+
type="button"
|
|
345
|
+
onClick={onAction}
|
|
346
|
+
className="text-sm font-medium text-m3-primary px-3 py-2 rounded-full hover:bg-m3-primary/8 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-m3-primary transition-colors whitespace-nowrap"
|
|
347
|
+
>
|
|
348
|
+
{actionLabel}
|
|
349
|
+
</button>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
{showDivider && (
|
|
354
|
+
<hr className="border-m3-outline-variant w-full shrink-0 m-0" />
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
<ScrollArea
|
|
358
|
+
type="hover"
|
|
359
|
+
className="flex-1 w-full"
|
|
360
|
+
viewportClassName="p-6"
|
|
361
|
+
>
|
|
362
|
+
{children}
|
|
363
|
+
</ScrollArea>
|
|
364
|
+
</m.div>
|
|
365
|
+
</RadixDialog.Content>
|
|
366
|
+
),
|
|
367
|
+
);
|
|
368
|
+
DialogFullScreenContent.displayName = "DialogFullScreenContent";
|
|
369
|
+
|
|
370
|
+
export {
|
|
371
|
+
Dialog,
|
|
372
|
+
DialogBody,
|
|
373
|
+
DialogClose,
|
|
374
|
+
DialogContent,
|
|
375
|
+
DialogDescription,
|
|
376
|
+
DialogFooter,
|
|
377
|
+
DialogFullScreenContent,
|
|
378
|
+
DialogHeader,
|
|
379
|
+
DialogIcon,
|
|
380
|
+
DialogOverlay,
|
|
381
|
+
DialogPortal,
|
|
382
|
+
DialogTitle,
|
|
383
|
+
DialogTrigger,
|
|
384
|
+
};
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import * as MotionReact from "motion/react";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { buildWavePath, Divider } from "./divider";
|
|
7
|
+
|
|
8
|
+
// Mock motion/react — same pattern as badge.test.tsx / chip.test.tsx
|
|
9
|
+
vi.mock("motion/react", async (importOriginal) => {
|
|
10
|
+
const actual = await importOriginal<typeof import("motion/react")>();
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
useReducedMotion: () => false,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// ── Test Suites ───────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
describe("Divider", () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// ── Core Accessibility ──────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe("core accessibility", () => {
|
|
27
|
+
it("renders with role='separator' by default", () => {
|
|
28
|
+
render(<Divider />);
|
|
29
|
+
expect(screen.getByRole("separator")).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("has aria-orientation='horizontal' by default", () => {
|
|
33
|
+
render(<Divider />);
|
|
34
|
+
expect(screen.getByRole("separator")).toHaveAttribute(
|
|
35
|
+
"aria-orientation",
|
|
36
|
+
"horizontal",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("vertical orientation sets aria-orientation='vertical'", () => {
|
|
41
|
+
render(<Divider orientation="vertical" />);
|
|
42
|
+
expect(screen.getByRole("separator")).toHaveAttribute(
|
|
43
|
+
"aria-orientation",
|
|
44
|
+
"vertical",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("decorative=true adds aria-hidden='true'", () => {
|
|
49
|
+
const { container } = render(<Divider decorative />);
|
|
50
|
+
const divider = container.firstChild as HTMLElement;
|
|
51
|
+
expect(divider).toHaveAttribute("aria-hidden", "true");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("decorative=true removes role='separator'", () => {
|
|
55
|
+
render(<Divider decorative />);
|
|
56
|
+
expect(screen.queryByRole("separator")).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ── Variants ────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
describe("variants", () => {
|
|
63
|
+
it("full-bleed → no indent classes", () => {
|
|
64
|
+
render(<Divider variant="full-bleed" />);
|
|
65
|
+
const el = screen.getByRole("separator");
|
|
66
|
+
expect(el.className).not.toMatch(/ml-|mx-|mt-|my-/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("inset + insetStart='standard' → has class 'ml-4'", () => {
|
|
70
|
+
render(<Divider variant="inset" insetStart="standard" />);
|
|
71
|
+
const el = screen.getByRole("separator");
|
|
72
|
+
expect(el.className).toContain("ml-4");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("inset + insetStart='icon' → has class 'ml-[72px]'", () => {
|
|
76
|
+
render(<Divider variant="inset" insetStart="icon" />);
|
|
77
|
+
const el = screen.getByRole("separator");
|
|
78
|
+
expect(el.className).toContain("ml-[72px]");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("middle-inset → has class 'mx-4'", () => {
|
|
82
|
+
render(<Divider variant="middle-inset" />);
|
|
83
|
+
const el = screen.getByRole("separator");
|
|
84
|
+
expect(el.className).toContain("mx-4");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("subheader → no indent classes (same as full-bleed)", () => {
|
|
88
|
+
render(<Divider variant="subheader" />);
|
|
89
|
+
const el = screen.getByRole("separator");
|
|
90
|
+
expect(el.className).not.toMatch(/ml-|mx-|mt-|my-/);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ── Shape: flat ─────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
describe("shape: flat", () => {
|
|
97
|
+
it("renders a div element (not svg)", () => {
|
|
98
|
+
const { container } = render(<Divider shape="flat" />);
|
|
99
|
+
// LazyMotion wrapper is present; the animated element is a div
|
|
100
|
+
const svg = container.querySelector("svg");
|
|
101
|
+
expect(svg).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("has class 'bg-m3-outline-variant'", () => {
|
|
105
|
+
render(<Divider shape="flat" />);
|
|
106
|
+
const el = screen.getByRole("separator");
|
|
107
|
+
expect(el.className).toContain("bg-m3-outline-variant");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("horizontal → has class 'h-px'", () => {
|
|
111
|
+
render(<Divider shape="flat" orientation="horizontal" />);
|
|
112
|
+
const el = screen.getByRole("separator");
|
|
113
|
+
expect(el.className).toContain("h-px");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("vertical → has class 'w-px'", () => {
|
|
117
|
+
render(<Divider shape="flat" orientation="vertical" />);
|
|
118
|
+
const el = screen.getByRole("separator");
|
|
119
|
+
expect(el.className).toContain("w-px");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ── Shape: wavy ─────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
describe("shape: wavy", () => {
|
|
126
|
+
it("renders an svg element", () => {
|
|
127
|
+
const { container } = render(<Divider shape="wavy" />);
|
|
128
|
+
const svg = container.querySelector("svg");
|
|
129
|
+
expect(svg).toBeInTheDocument();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("svg contains a path element with d attribute starting with 'M'", () => {
|
|
133
|
+
const { container } = render(<Divider shape="wavy" />);
|
|
134
|
+
const path = container.querySelector("path");
|
|
135
|
+
expect(path).toBeInTheDocument();
|
|
136
|
+
expect(path?.getAttribute("d")).toMatch(/^M/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("wavy path has strokeLinecap='round' for rounded ends", () => {
|
|
140
|
+
const { container } = render(<Divider shape="wavy" />);
|
|
141
|
+
const path = container.querySelector("path");
|
|
142
|
+
expect(path?.getAttribute("stroke-linecap")).toBe("round");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("wavy renders direct <path> — no <pattern> element", () => {
|
|
146
|
+
const { container } = render(<Divider shape="wavy" />);
|
|
147
|
+
expect(container.querySelector("pattern")).toBeNull();
|
|
148
|
+
expect(container.querySelector("defs")).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("wavy + vertical → falls back to flat (no svg rendered)", () => {
|
|
152
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
153
|
+
const { container } = render(
|
|
154
|
+
<Divider shape="wavy" orientation="vertical" />,
|
|
155
|
+
);
|
|
156
|
+
const svg = container.querySelector("svg");
|
|
157
|
+
expect(svg).toBeNull();
|
|
158
|
+
warnSpy.mockRestore();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("wavy + vertical → emits console.warn", () => {
|
|
162
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
163
|
+
render(<Divider shape="wavy" orientation="vertical" />);
|
|
164
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
165
|
+
expect.stringContaining(
|
|
166
|
+
"shape='wavy' is not supported with orientation='vertical'",
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
warnSpy.mockRestore();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("custom waveConfig → renders without error", () => {
|
|
173
|
+
expect(() => {
|
|
174
|
+
render(
|
|
175
|
+
<Divider
|
|
176
|
+
shape="wavy"
|
|
177
|
+
waveConfig={{ amplitude: 5, wavelength: 24 }}
|
|
178
|
+
/>,
|
|
179
|
+
);
|
|
180
|
+
}).not.toThrow();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ── buildWavePath Helper ────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
describe("buildWavePath helper", () => {
|
|
187
|
+
it("returns a string starting with 'M {startX},{yCenter}'", () => {
|
|
188
|
+
// New signature: buildWavePath(startX, endX, amplitude, wavelength, yCenter)
|
|
189
|
+
const d = buildWavePath(0, 64, 3, 16, 4);
|
|
190
|
+
expect(d).toMatch(/^M 0,4/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("respects non-zero startX", () => {
|
|
194
|
+
const d = buildWavePath(0.5, 100, 2, 32, 4);
|
|
195
|
+
expect(d).toMatch(/^M 0\.5,4/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("contains 'C' (cubic Bézier curves)", () => {
|
|
199
|
+
const d = buildWavePath(0, 64, 3, 16, 4);
|
|
200
|
+
expect(d).toContain("C");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("produces path with monotonically increasing X coordinates", () => {
|
|
204
|
+
const d = buildWavePath(0, 64, 3, 16, 4);
|
|
205
|
+
const parts = d.replace(/[MC]/g, " ").trim().split(/\s+/);
|
|
206
|
+
const xValues: number[] = [];
|
|
207
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
208
|
+
const x = parseFloat(parts[i] ?? "");
|
|
209
|
+
if (!Number.isNaN(x)) xValues.push(x);
|
|
210
|
+
}
|
|
211
|
+
expect(xValues.length).toBeGreaterThan(0);
|
|
212
|
+
expect(xValues[xValues.length - 1]).toBeGreaterThan(xValues[0] ?? 0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("returns empty string when startX >= endX", () => {
|
|
216
|
+
expect(buildWavePath(0, 0)).toBe("");
|
|
217
|
+
expect(buildWavePath(10, 5)).toBe("");
|
|
218
|
+
expect(buildWavePath(0, -10)).toBe("");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("respects custom amplitude and wavelength", () => {
|
|
222
|
+
const d = buildWavePath(0, 32, 5, 8, 5);
|
|
223
|
+
expect(d).toMatch(/^M 0,5/);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("path ends exactly at endX", () => {
|
|
227
|
+
const d = buildWavePath(0, 64, 2, 32, 4);
|
|
228
|
+
// Last coordinate pair in the path should be endX,yCenter
|
|
229
|
+
const match = d.match(/(\d+(?:\.\d+)?),(\d+(?:\.\d+)?)$/);
|
|
230
|
+
expect(Number(match?.[1])).toBe(64);
|
|
231
|
+
expect(Number(match?.[2])).toBe(4);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ── Props Forwarding ────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
describe("props forwarding", () => {
|
|
238
|
+
it("custom className is merged via cn()", () => {
|
|
239
|
+
render(<Divider className="my-custom-divider" />);
|
|
240
|
+
const el = screen.getByRole("separator");
|
|
241
|
+
expect(el).toHaveClass("my-custom-divider");
|
|
242
|
+
// Also retains base classes
|
|
243
|
+
expect(el.className).toContain("bg-m3-outline-variant");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("style prop is forwarded to the root element", () => {
|
|
247
|
+
render(<Divider style={{ opacity: 0.5 }} />);
|
|
248
|
+
const el = screen.getByRole("separator");
|
|
249
|
+
expect(el.style.opacity).toBe("0.5");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("ref forwarding works for flat divider (div element)", () => {
|
|
253
|
+
const ref = { current: null };
|
|
254
|
+
render(<Divider ref={ref} />);
|
|
255
|
+
expect(ref.current).not.toBeNull();
|
|
256
|
+
expect((ref.current as unknown as HTMLElement).tagName).toBe("DIV");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("ref forwarding works for wavy divider (outer div element)", () => {
|
|
260
|
+
const ref = { current: null };
|
|
261
|
+
render(<Divider ref={ref} shape="wavy" />);
|
|
262
|
+
expect(ref.current).not.toBeNull();
|
|
263
|
+
// Wavy forwards ref to the outer wrapper div
|
|
264
|
+
expect((ref.current as unknown as HTMLElement).tagName).toBe("DIV");
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ── Reduced Motion ──────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
describe("prefers-reduced-motion", () => {
|
|
271
|
+
it("renders correctly when useReducedMotion returns true", () => {
|
|
272
|
+
vi.spyOn(MotionReact, "useReducedMotion").mockReturnValue(true);
|
|
273
|
+
render(<Divider />);
|
|
274
|
+
expect(screen.getByRole("separator")).toBeInTheDocument();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("wavy renders correctly when useReducedMotion returns true", () => {
|
|
278
|
+
vi.spyOn(MotionReact, "useReducedMotion").mockReturnValue(true);
|
|
279
|
+
const { container } = render(<Divider shape="wavy" />);
|
|
280
|
+
expect(container.querySelector("svg")).toBeInTheDocument();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ── Snapshots ───────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
describe("snapshots", () => {
|
|
287
|
+
it("flat/horizontal (default)", () => {
|
|
288
|
+
const { container } = render(<Divider />);
|
|
289
|
+
expect(container.firstChild).toMatchSnapshot();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("flat/vertical", () => {
|
|
293
|
+
const { container } = render(<Divider orientation="vertical" />);
|
|
294
|
+
expect(container.firstChild).toMatchSnapshot();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("wavy", () => {
|
|
298
|
+
const { container } = render(<Divider shape="wavy" />);
|
|
299
|
+
expect(container.firstChild).toMatchSnapshot();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("inset", () => {
|
|
303
|
+
const { container } = render(
|
|
304
|
+
<Divider variant="inset" insetStart="standard" />,
|
|
305
|
+
);
|
|
306
|
+
expect(container.firstChild).toMatchSnapshot();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("middle-inset", () => {
|
|
310
|
+
const { container } = render(<Divider variant="middle-inset" />);
|
|
311
|
+
expect(container.firstChild).toMatchSnapshot();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|