@bug-on/md3-react 3.0.1 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +42 -42
- package/CHANGELOG.md +10 -0
- package/dist/index.css +107 -0
- package/dist/index.d.mts +1491 -1053
- package/dist/index.d.ts +1491 -1053
- package/dist/index.js +4457 -3156
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4394 -3109
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -6
- package/scripts/copy-assets.js +113 -8
- package/src/index.ts +66 -18
- package/src/test/button.test.tsx +1 -1
- package/src/ui/app-bar/app-bar.tokens.ts +5 -24
- package/src/ui/badge.tsx +2 -1
- package/src/ui/buttons/button/button-tokens.ts +118 -0
- package/src/ui/{button.test.tsx → buttons/button/button.test.tsx} +0 -21
- package/src/ui/buttons/button/button.tsx +381 -0
- package/src/ui/buttons/button/index.ts +3 -0
- package/src/ui/buttons/button/types.ts +90 -0
- package/src/ui/buttons/button-group/button-group-defaults.ts +95 -0
- package/src/ui/buttons/button-group/button-group-tokens.ts +20 -0
- package/src/ui/{button-group.test.tsx → buttons/button-group/button-group.test.tsx} +9 -10
- package/src/ui/buttons/button-group/button-group.tsx +699 -0
- package/src/ui/buttons/button-group/index.ts +8 -0
- package/src/ui/buttons/button-group/types.ts +77 -0
- package/src/ui/{fab.tsx → buttons/fabs/fab/fab.tsx} +6 -6
- package/src/ui/buttons/fabs/fab/index.ts +1 -0
- package/src/ui/{fab-menu.tsx → buttons/fabs/fab-menu/fab-menu.tsx} +7 -4
- package/src/ui/buttons/fabs/fab-menu/index.ts +1 -0
- package/src/ui/buttons/fabs/index.ts +2 -0
- package/src/ui/{icon-button.tsx → buttons/icon-button/icon-button.tsx} +6 -6
- package/src/ui/buttons/icon-button/index.ts +1 -0
- package/src/ui/buttons/index.ts +4 -0
- package/src/ui/code-block.tsx +1 -1
- package/src/ui/dialog.tsx +4 -7
- package/src/ui/drawer.tsx +4 -7
- package/src/ui/menu/menu-animations.ts +14 -20
- package/src/ui/menu/menu-tokens.ts +7 -5
- package/src/ui/menu/menu.test.tsx +9 -4
- package/src/ui/navigation-bar.test.tsx +111 -0
- package/src/ui/navigation-bar.tsx +464 -0
- package/src/ui/navigation-rail.test.tsx +5 -4
- package/src/ui/navigation-rail.tsx +32 -23
- package/src/ui/scroll-area.tsx +4 -0
- package/src/ui/search/search-view-fullscreen.tsx +1 -1
- package/src/ui/search/search.tokens.ts +9 -43
- package/src/ui/search/trailing-action.tsx +1 -1
- package/src/ui/shared/constants.ts +25 -27
- package/src/ui/shared/motion-tokens.ts +238 -0
- package/src/ui/snackbar/snackbar.tsx +4 -6
- package/src/ui/switch/switch.tsx +12 -18
- package/src/ui/text-field/text-field.tokens.ts +12 -12
- package/src/ui/text-field/text-field.tsx +31 -19
- package/src/ui/theme-provider/index.tsx +1 -5
- package/src/ui/toc.tsx +1 -1
- package/src/ui/toolbar/__snapshots__/bottom-docked-toolbar.test.tsx.snap +51 -0
- package/src/ui/toolbar/__snapshots__/floating-toolbar-with-fab.test.tsx.snap +113 -0
- package/src/ui/toolbar/__snapshots__/floating-toolbar.test.tsx.snap +169 -0
- package/src/ui/toolbar/bottom-docked-toolbar.test.tsx +114 -0
- package/src/ui/toolbar/docked-toolbar.tsx +186 -0
- package/src/ui/toolbar/floating-toolbar-with-fab.test.tsx +139 -0
- package/src/ui/toolbar/floating-toolbar-with-fab.tsx +199 -0
- package/src/ui/toolbar/floating-toolbar.test.tsx +230 -0
- package/src/ui/toolbar/floating-toolbar.tsx +344 -0
- package/src/ui/toolbar/index.ts +35 -0
- package/src/ui/toolbar/toolbar-colors.ts +37 -0
- package/src/ui/toolbar/toolbar-context.tsx +13 -0
- package/src/ui/toolbar/toolbar-divider.test.tsx +54 -0
- package/src/ui/toolbar/toolbar-divider.tsx +73 -0
- package/src/ui/toolbar/toolbar-icon-button.test.tsx +68 -0
- package/src/ui/toolbar/toolbar-icon-button.tsx +136 -0
- package/src/ui/toolbar/toolbar-scroll-behavior.ts +140 -0
- package/src/ui/toolbar/toolbar-tokens.ts +51 -0
- package/test-clip.html +31 -0
- package/test-shadow.html +5 -1
- package/test-width.html +34 -0
- package/src/ui/button-group.tsx +0 -350
- package/src/ui/button.tsx +0 -665
- package/test-render.tsx +0 -4
- package/test_output.txt +0 -164
- package/test_output_v2.txt +0 -5
- /package/src/ui/{fab.test.tsx → buttons/fabs/fab/fab.test.tsx} +0 -0
- /package/src/ui/{fab-menu.test.tsx → buttons/fabs/fab-menu/fab-menu.test.tsx} +0 -0
- /package/src/ui/{icon-button.test.tsx → buttons/icon-button/icon-button.test.tsx} +0 -0
- /package/src/ui/{Text.tsx → text.tsx} +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import type { MD3Size } from "../../../types/md3";
|
|
3
|
+
|
|
4
|
+
export type ButtonGroupVariant = "standard" | "connected" | "navbar";
|
|
5
|
+
export type ButtonGroupOrientation = "horizontal" | "vertical";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Thuộc tính truyền vào cho thành phần nhóm nút (Button Group).
|
|
9
|
+
*/
|
|
10
|
+
export interface ButtonGroupProps
|
|
11
|
+
extends React.FieldsetHTMLAttributes<HTMLFieldSetElement> {
|
|
12
|
+
/**
|
|
13
|
+
* Cấu trúc hiển thị của nhóm nút:
|
|
14
|
+
* - `standard`: Các nút cách xa và có khoảng cách độc lập với nhau (gap).
|
|
15
|
+
* - `connected`: Các nút nối liền khung viền với nhau để tạo thành dạng Segmented Button.
|
|
16
|
+
* - `navbar`: Nút hiển thị dưới dạng thanh điều hướng với active pill chuyển động (Sliding Indicator).
|
|
17
|
+
* @default "standard"
|
|
18
|
+
*/
|
|
19
|
+
variant?: ButtonGroupVariant;
|
|
20
|
+
/**
|
|
21
|
+
* Hướng sắp xếp của các nút trong nhóm.
|
|
22
|
+
* @default "horizontal"
|
|
23
|
+
*/
|
|
24
|
+
orientation?: ButtonGroupOrientation;
|
|
25
|
+
/**
|
|
26
|
+
* Đặt thành `true` nếu bạn muốn nhóm hiển thị dạng `standard` giãn đều lấp đầy toàn bộ khu vực chứa (container).
|
|
27
|
+
* @default false
|
|
28
|
+
*/
|
|
29
|
+
fullWidth?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Áp dụng thống nhất chung một kích thước (`size`) cho tất cả các con trong nhóm (ghi đè kích thước lẻ từng nút).
|
|
32
|
+
*/
|
|
33
|
+
size?: MD3Size;
|
|
34
|
+
/**
|
|
35
|
+
* Bật/tắt hiệu ứng thu phóng độ rộng / khoảng đệm (Morphing Width) khi nhấn vào các nút (áp dụng cho nhóm `standard`).
|
|
36
|
+
* @default true
|
|
37
|
+
*/
|
|
38
|
+
morphingWidth?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Tỷ lệ mở rộng chiều rộng của nút khi được nhấn (áp dụng cho nhóm `standard` ngang).
|
|
41
|
+
* Theo MD3 Spec, mặc định là 0.15 (15%).
|
|
42
|
+
* @default 0.15
|
|
43
|
+
*/
|
|
44
|
+
expandedRatio?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Tự động hiển thị biểu tượng (icon) Check khi một nút trạng thái nằm trong nhóm được chỉ định là `selected={true}`.
|
|
47
|
+
* @default false
|
|
48
|
+
*/
|
|
49
|
+
showCheck?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Điều khiển hành vi hiển thị Icon (áp dụng chính cho `navbar`).
|
|
52
|
+
* - `selected`: Chỉ hiển thị icon trên mục được chọn.
|
|
53
|
+
* - `all`: Hiển thị trên tất cả.
|
|
54
|
+
* - `none`: Ẩn toàn bộ icon.
|
|
55
|
+
*/
|
|
56
|
+
iconBehavior?: "selected" | "all" | "none";
|
|
57
|
+
/**
|
|
58
|
+
* Điều khiển hành vi hiển thị Label (áp dụng chính cho `navbar`).
|
|
59
|
+
* - `selected`: Chỉ hiển thị label trên mục được chọn.
|
|
60
|
+
* - `all`: Hiển thị trên tất cả.
|
|
61
|
+
* - `none`: Ẩn toàn bộ label.
|
|
62
|
+
*/
|
|
63
|
+
labelBehavior?: "selected" | "all" | "none";
|
|
64
|
+
/**
|
|
65
|
+
* Tùy chọn: Bật chế độ Active Morphing (dựa trên Selection).
|
|
66
|
+
* Khi `true`, các nút sẽ tự động co dãn (tăng/giảm width) dựa trên việc nút nào đang được chọn,
|
|
67
|
+
* tạo hiệu ứng thay đổi kích thước mượt mà theo trạng thái.
|
|
68
|
+
* Khi `false` hoặc `undefined`, Group sẽ hoạt động dựa trên `isPressed` (nhấn chuột) như mặc định.
|
|
69
|
+
*/
|
|
70
|
+
activeMorphing?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Class CSS tùy chỉnh áp dụng cho từng button con trong nhóm.
|
|
73
|
+
* Hữu ích để thay đổi padding, min-width hoặc các thuộc tính khác một cách linh hoạt.
|
|
74
|
+
* Đối với variant `navbar`, class này sẽ được ưu tiên hơn so với các padding mặc định.
|
|
75
|
+
*/
|
|
76
|
+
itemClassName?: string;
|
|
77
|
+
}
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
import type { HTMLMotionProps } from "motion/react";
|
|
14
14
|
import { AnimatePresence, domMax, LazyMotion, m } from "motion/react";
|
|
15
15
|
import * as React from "react";
|
|
16
|
-
import { cn } from "
|
|
17
|
-
import { LoadingIndicator } from "
|
|
18
|
-
import { ProgressIndicator } from "
|
|
19
|
-
import { Ripple, useRippleState } from "
|
|
16
|
+
import { cn } from "../../../../lib/utils";
|
|
17
|
+
import { LoadingIndicator } from "../../../loading-indicator";
|
|
18
|
+
import { ProgressIndicator } from "../../../progress-indicator";
|
|
19
|
+
import { Ripple, useRippleState } from "../../../ripple";
|
|
20
20
|
import {
|
|
21
21
|
ICON_SPAN_VARIANTS,
|
|
22
22
|
SPRING_TRANSITION,
|
|
23
23
|
SPRING_TRANSITION_FAST,
|
|
24
|
-
} from "
|
|
25
|
-
import { TouchTarget } from "
|
|
24
|
+
} from "../../../shared/constants";
|
|
25
|
+
import { TouchTarget } from "../../../shared/touch-target";
|
|
26
26
|
|
|
27
27
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
28
|
// Design Tokens
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./fab";
|
|
@@ -20,10 +20,13 @@ import {
|
|
|
20
20
|
useTransform,
|
|
21
21
|
} from "motion/react";
|
|
22
22
|
import * as React from "react";
|
|
23
|
-
import { cn } from "
|
|
24
|
-
import { Ripple, useRippleState } from "
|
|
25
|
-
import {
|
|
26
|
-
|
|
23
|
+
import { cn } from "../../../../lib/utils";
|
|
24
|
+
import { Ripple, useRippleState } from "../../../ripple";
|
|
25
|
+
import {
|
|
26
|
+
SPRING_TRANSITION,
|
|
27
|
+
SPRING_TRANSITION_FAST,
|
|
28
|
+
} from "../../../shared/constants";
|
|
29
|
+
import { TouchTarget } from "../../../shared/touch-target";
|
|
27
30
|
|
|
28
31
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
32
|
// Design Tokens — MD3 FAB Menu Spec
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./fab-menu";
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
import type { HTMLMotionProps } from "motion/react";
|
|
14
14
|
import { AnimatePresence, domMax, LazyMotion, m } from "motion/react";
|
|
15
15
|
import * as React from "react";
|
|
16
|
-
import { cn } from "
|
|
17
|
-
import { LoadingIndicator } from "
|
|
18
|
-
import { ProgressIndicator } from "
|
|
19
|
-
import { Ripple, useRippleState } from "
|
|
16
|
+
import { cn } from "../../../lib/utils";
|
|
17
|
+
import { LoadingIndicator } from "../../loading-indicator";
|
|
18
|
+
import { ProgressIndicator } from "../../progress-indicator";
|
|
19
|
+
import { Ripple, useRippleState } from "../../ripple";
|
|
20
20
|
import {
|
|
21
21
|
ICON_SPAN_VARIANTS,
|
|
22
22
|
SPRING_TRANSITION,
|
|
23
23
|
SPRING_TRANSITION_FAST,
|
|
24
|
-
} from "
|
|
25
|
-
import { TouchTarget } from "
|
|
24
|
+
} from "../../shared/constants";
|
|
25
|
+
import { TouchTarget } from "../../shared/touch-target";
|
|
26
26
|
|
|
27
27
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
28
|
// Design Tokens
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./icon-button";
|
package/src/ui/code-block.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { createElement, useCallback, useEffect, useState } from "react";
|
|
4
4
|
import { cn } from "../lib/utils";
|
|
5
|
-
import { Button } from "./button";
|
|
5
|
+
import { Button } from "./buttons/button";
|
|
6
6
|
import { Icon } from "./icon";
|
|
7
7
|
import { ScrollArea } from "./scroll-area";
|
|
8
8
|
|
package/src/ui/dialog.tsx
CHANGED
|
@@ -14,17 +14,14 @@ import * as RadixDialog from "@radix-ui/react-dialog";
|
|
|
14
14
|
import { AnimatePresence, m } from "motion/react";
|
|
15
15
|
import * as React from "react";
|
|
16
16
|
import { cn } from "../lib/utils";
|
|
17
|
+
import { IconButton } from "./buttons/icon-button";
|
|
17
18
|
import { Icon } from "./icon";
|
|
18
|
-
import { IconButton } from "./icon-button";
|
|
19
19
|
import { ScrollArea } from "./scroll-area";
|
|
20
|
+
import { DEFAULT_SPATIAL_SPRING } from "./shared/motion-tokens";
|
|
20
21
|
|
|
21
22
|
// ─── MD3 Spring Config (Expressive) ──────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
stiffness: 400,
|
|
25
|
-
damping: 30,
|
|
26
|
-
mass: 1,
|
|
27
|
-
};
|
|
23
|
+
// MD3 default.spatial: stiffness=380, dampingRatio=0.8 → Framer damping ≈ 31.19
|
|
24
|
+
const MD3_SPRING = DEFAULT_SPATIAL_SPRING;
|
|
28
25
|
|
|
29
26
|
const MD3_OVERLAY_ANIM = {
|
|
30
27
|
initial: { opacity: 0 },
|
package/src/ui/drawer.tsx
CHANGED
|
@@ -3,15 +3,12 @@ import { AnimatePresence, m } from "motion/react";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../lib/utils";
|
|
5
5
|
import { Icon } from "./icon";
|
|
6
|
+
import { DEFAULT_SPATIAL_SPRING } from "./shared/motion-tokens";
|
|
6
7
|
|
|
7
8
|
// ─── MD3 Expressive Drawer Animation ─────────────────────────────────────────
|
|
8
|
-
// Slide
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
stiffness: 350,
|
|
12
|
-
damping: 35,
|
|
13
|
-
mass: 0.9,
|
|
14
|
-
};
|
|
9
|
+
// Slide from bottom, MD3 default.spatial spring
|
|
10
|
+
// (stiffness=380, dampingRatio=0.8 → Framer damping ≈ 31.19)
|
|
11
|
+
const MD3_DRAWER_SPRING = DEFAULT_SPATIAL_SPRING;
|
|
15
12
|
|
|
16
13
|
const MD3_DRAWER_ANIM = {
|
|
17
14
|
initial: { y: "100%", opacity: 0.6 },
|
|
@@ -1,28 +1,22 @@
|
|
|
1
1
|
// ─── MD3 Expressive Menu — Framer Motion Animation Variants ──────────────────
|
|
2
|
-
// FastSpatial:
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// Android: duration=150ms, FastOutLinearIn
|
|
7
|
-
// Framer: duration=0.15, ease=[0.4, 0, 1, 1]
|
|
2
|
+
// FastSpatial: md.sys.motion.spring.fast.spatial
|
|
3
|
+
// MD3: stiffness=800, dampingRatio=0.6 → Framer damping ≈ 33.94
|
|
4
|
+
// FastEffects: md.sys.motion.spring.fast.effects (CSS fallback used for exit)
|
|
5
|
+
// MD3: duration=150ms, cubic-bezier(0.31, 0.94, 0.34, 1.00)
|
|
8
6
|
|
|
9
|
-
import type {
|
|
7
|
+
import type { Variants } from "motion/react";
|
|
8
|
+
import {
|
|
9
|
+
FAST_EFFECTS_SPRING,
|
|
10
|
+
FAST_SPATIAL_SPRING,
|
|
11
|
+
} from "../shared/motion-tokens";
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
export { FAST_EFFECTS_SPRING, FAST_SPATIAL_SPRING };
|
|
12
14
|
|
|
13
|
-
/**
|
|
14
|
-
export const
|
|
15
|
-
type: "spring",
|
|
16
|
-
stiffness: 380,
|
|
17
|
-
damping: 28,
|
|
18
|
-
mass: 1,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/** FastEffects transition — used for opacity and exit animations */
|
|
22
|
-
export const FAST_EFFECTS_TRANSITION: Transition = {
|
|
15
|
+
/** FastEffects CSS-fallback transition — used for opacity/exit animations */
|
|
16
|
+
export const FAST_EFFECTS_TRANSITION = {
|
|
23
17
|
duration: 0.15,
|
|
24
|
-
ease: [0.
|
|
25
|
-
};
|
|
18
|
+
ease: [0.31, 0.94, 0.34, 1.0] as [number, number, number, number],
|
|
19
|
+
} as const;
|
|
26
20
|
|
|
27
21
|
// ─── Menu popup container ─────────────────────────────────────────────────────
|
|
28
22
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Sourced from: SegmentedMenuTokens.kt, StandardMenuTokens.kt, VibrantMenuTokens.kt,
|
|
3
3
|
// MenuTokens.kt, MenuDefaults.kt, ListTokens.kt
|
|
4
4
|
|
|
5
|
+
import { cornerRadius } from "@bug-on/md3-tokens";
|
|
6
|
+
|
|
5
7
|
// ─── Spacing (px → dp) ────────────────────────────────────────────────────────
|
|
6
8
|
|
|
7
9
|
/** Horizontal padding for menu items: 16dp (ItemLeadingSpace / ItemTrailingSpace) */
|
|
@@ -64,23 +66,23 @@ export const BASELINE_ITEM_SHAPE = "rounded-none";
|
|
|
64
66
|
*/
|
|
65
67
|
export const GROUP_SHAPES = {
|
|
66
68
|
/** Active standalone group shape: CornerLarge all corners (16px) */
|
|
67
|
-
standaloneActive:
|
|
69
|
+
standaloneActive: `${cornerRadius.large}px`,
|
|
68
70
|
/**
|
|
69
71
|
* Active leading group shape: top=CornerLarge(16px), bottom=CornerSmall(8px)
|
|
70
72
|
* Source: SegmentedMenuTokens — LeadingContainerShape:
|
|
71
73
|
* topStart=CornerLarge, topEnd=CornerLarge, bottomStart=CornerSmall, bottomEnd=CornerSmall
|
|
72
74
|
*/
|
|
73
|
-
leadingActive:
|
|
75
|
+
leadingActive: `${cornerRadius.large}px ${cornerRadius.large}px ${cornerRadius.small}px ${cornerRadius.small}px`,
|
|
74
76
|
/** Active middle group shape: CornerExtraSmall all corners (4px) */
|
|
75
|
-
middleActive:
|
|
77
|
+
middleActive: `${cornerRadius.extraSmall}px`,
|
|
76
78
|
/**
|
|
77
79
|
* Active trailing group shape: top=CornerSmall(8px), bottom=CornerLarge(16px)
|
|
78
80
|
* Source: SegmentedMenuTokens — TrailingContainerShape:
|
|
79
81
|
* topStart=CornerSmall, topEnd=CornerSmall, bottomStart=CornerLarge, bottomEnd=CornerLarge
|
|
80
82
|
*/
|
|
81
|
-
trailingActive:
|
|
83
|
+
trailingActive: `${cornerRadius.small}px ${cornerRadius.small}px ${cornerRadius.large}px ${cornerRadius.large}px`,
|
|
82
84
|
/** Inactive (default, pre-hover) shape for all groups: CornerExtraSmall (4px) */
|
|
83
|
-
inactive:
|
|
85
|
+
inactive: `${cornerRadius.extraSmall}px`,
|
|
84
86
|
} as const;
|
|
85
87
|
|
|
86
88
|
/**
|
|
@@ -616,9 +616,14 @@ describe("Menu Internals", () => {
|
|
|
616
616
|
expect(screen.getByText("A")).toBeInTheDocument();
|
|
617
617
|
});
|
|
618
618
|
|
|
619
|
-
// 3. Animation variants check
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
619
|
+
// 3. Animation variants check — values must match MD3 fast.spatial token
|
|
620
|
+
// md.sys.motion.spring.fast.spatial: stiffness=800, dampingRatio=0.6
|
|
621
|
+
// Framer damping = 2 × 0.6 × √800 ≈ 33.94
|
|
622
|
+
it("FAST_SPATIAL_SPRING has correct MD3 spring parameters", () => {
|
|
623
|
+
expect(FAST_SPATIAL_SPRING.stiffness).toBe(800);
|
|
624
|
+
expect((FAST_SPATIAL_SPRING as { damping: number }).damping).toBeCloseTo(
|
|
625
|
+
33.94,
|
|
626
|
+
1,
|
|
627
|
+
);
|
|
623
628
|
});
|
|
624
629
|
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { NavigationBar, NavigationBarItem } from "./navigation-bar";
|
|
4
|
+
|
|
5
|
+
describe("NavigationBar", () => {
|
|
6
|
+
it("renders with correct role and aria-label", () => {
|
|
7
|
+
render(
|
|
8
|
+
<NavigationBar>
|
|
9
|
+
<NavigationBarItem selected icon={<span />} label="Home" />
|
|
10
|
+
</NavigationBar>,
|
|
11
|
+
);
|
|
12
|
+
const nav = screen.getByRole("navigation", { name: "Main navigation" });
|
|
13
|
+
expect(nav).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("renders correct number of items", () => {
|
|
17
|
+
render(
|
|
18
|
+
<NavigationBar>
|
|
19
|
+
<NavigationBarItem selected icon={<span />} label="Home" />
|
|
20
|
+
<NavigationBarItem selected={false} icon={<span />} label="Search" />
|
|
21
|
+
</NavigationBar>,
|
|
22
|
+
);
|
|
23
|
+
const items = screen.getAllByRole("menuitem");
|
|
24
|
+
expect(items).toHaveLength(2);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('marks selected item with aria-current="page"', () => {
|
|
28
|
+
render(
|
|
29
|
+
<NavigationBar>
|
|
30
|
+
<NavigationBarItem selected icon={<span />} label="Home" />
|
|
31
|
+
<NavigationBarItem selected={false} icon={<span />} label="Search" />
|
|
32
|
+
</NavigationBar>,
|
|
33
|
+
);
|
|
34
|
+
const selectedItem = screen.getByRole("menuitem", { current: "page" });
|
|
35
|
+
expect(selectedItem).toHaveTextContent("Home");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("calls onClick when item clicked", () => {
|
|
39
|
+
const onClick = vi.fn();
|
|
40
|
+
render(
|
|
41
|
+
<NavigationBar>
|
|
42
|
+
<NavigationBarItem
|
|
43
|
+
selected={false}
|
|
44
|
+
icon={<span />}
|
|
45
|
+
label="Home"
|
|
46
|
+
onClick={onClick}
|
|
47
|
+
/>
|
|
48
|
+
</NavigationBar>,
|
|
49
|
+
);
|
|
50
|
+
const item = screen.getByRole("menuitem");
|
|
51
|
+
fireEvent.click(item);
|
|
52
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("does not call onClick when disabled", () => {
|
|
56
|
+
const onClick = vi.fn();
|
|
57
|
+
render(
|
|
58
|
+
<NavigationBar>
|
|
59
|
+
<NavigationBarItem
|
|
60
|
+
selected={false}
|
|
61
|
+
disabled
|
|
62
|
+
icon={<span />}
|
|
63
|
+
label="Home"
|
|
64
|
+
onClick={onClick}
|
|
65
|
+
/>
|
|
66
|
+
</NavigationBar>,
|
|
67
|
+
);
|
|
68
|
+
const item = screen.getByRole("menuitem");
|
|
69
|
+
fireEvent.click(item);
|
|
70
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
71
|
+
expect(item).toHaveAttribute("aria-disabled", "true");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("renders badge when provided", () => {
|
|
75
|
+
render(
|
|
76
|
+
<NavigationBar>
|
|
77
|
+
<NavigationBarItem selected icon={<span />} label="Home" badge="1" />
|
|
78
|
+
</NavigationBar>,
|
|
79
|
+
);
|
|
80
|
+
const badge = screen.getByText("1");
|
|
81
|
+
expect(badge).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("uses aria-label prop when provided", () => {
|
|
85
|
+
render(
|
|
86
|
+
<NavigationBar>
|
|
87
|
+
<NavigationBarItem
|
|
88
|
+
selected
|
|
89
|
+
icon={<span />}
|
|
90
|
+
label="Home"
|
|
91
|
+
aria-label="Go to homepage"
|
|
92
|
+
/>
|
|
93
|
+
</NavigationBar>,
|
|
94
|
+
);
|
|
95
|
+
const item = screen.getByRole("menuitem");
|
|
96
|
+
expect(item).toHaveAttribute("aria-label", "Go to homepage");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("renders xr variant with correct classes", () => {
|
|
100
|
+
render(
|
|
101
|
+
<NavigationBar variant="xr">
|
|
102
|
+
<NavigationBarItem selected icon={<span />} label="Home" />
|
|
103
|
+
</NavigationBar>,
|
|
104
|
+
);
|
|
105
|
+
const nav = screen.getByRole("navigation", { name: "Main navigation" });
|
|
106
|
+
expect(nav).toHaveClass("bottom-6");
|
|
107
|
+
expect(nav).toHaveClass("left-1/2");
|
|
108
|
+
expect(nav).toHaveClass("-translate-x-1/2");
|
|
109
|
+
expect(nav).toHaveClass("rounded-[48px]");
|
|
110
|
+
});
|
|
111
|
+
});
|