@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.
Files changed (86) hide show
  1. package/.turbo/turbo-build.log +42 -42
  2. package/CHANGELOG.md +10 -0
  3. package/dist/index.css +107 -0
  4. package/dist/index.d.mts +1491 -1053
  5. package/dist/index.d.ts +1491 -1053
  6. package/dist/index.js +4457 -3156
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +4394 -3109
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +11 -6
  11. package/scripts/copy-assets.js +113 -8
  12. package/src/index.ts +66 -18
  13. package/src/test/button.test.tsx +1 -1
  14. package/src/ui/app-bar/app-bar.tokens.ts +5 -24
  15. package/src/ui/badge.tsx +2 -1
  16. package/src/ui/buttons/button/button-tokens.ts +118 -0
  17. package/src/ui/{button.test.tsx → buttons/button/button.test.tsx} +0 -21
  18. package/src/ui/buttons/button/button.tsx +381 -0
  19. package/src/ui/buttons/button/index.ts +3 -0
  20. package/src/ui/buttons/button/types.ts +90 -0
  21. package/src/ui/buttons/button-group/button-group-defaults.ts +95 -0
  22. package/src/ui/buttons/button-group/button-group-tokens.ts +20 -0
  23. package/src/ui/{button-group.test.tsx → buttons/button-group/button-group.test.tsx} +9 -10
  24. package/src/ui/buttons/button-group/button-group.tsx +699 -0
  25. package/src/ui/buttons/button-group/index.ts +8 -0
  26. package/src/ui/buttons/button-group/types.ts +77 -0
  27. package/src/ui/{fab.tsx → buttons/fabs/fab/fab.tsx} +6 -6
  28. package/src/ui/buttons/fabs/fab/index.ts +1 -0
  29. package/src/ui/{fab-menu.tsx → buttons/fabs/fab-menu/fab-menu.tsx} +7 -4
  30. package/src/ui/buttons/fabs/fab-menu/index.ts +1 -0
  31. package/src/ui/buttons/fabs/index.ts +2 -0
  32. package/src/ui/{icon-button.tsx → buttons/icon-button/icon-button.tsx} +6 -6
  33. package/src/ui/buttons/icon-button/index.ts +1 -0
  34. package/src/ui/buttons/index.ts +4 -0
  35. package/src/ui/code-block.tsx +1 -1
  36. package/src/ui/dialog.tsx +4 -7
  37. package/src/ui/drawer.tsx +4 -7
  38. package/src/ui/menu/menu-animations.ts +14 -20
  39. package/src/ui/menu/menu-tokens.ts +7 -5
  40. package/src/ui/menu/menu.test.tsx +9 -4
  41. package/src/ui/navigation-bar.test.tsx +111 -0
  42. package/src/ui/navigation-bar.tsx +464 -0
  43. package/src/ui/navigation-rail.test.tsx +5 -4
  44. package/src/ui/navigation-rail.tsx +32 -23
  45. package/src/ui/scroll-area.tsx +4 -0
  46. package/src/ui/search/search-view-fullscreen.tsx +1 -1
  47. package/src/ui/search/search.tokens.ts +9 -43
  48. package/src/ui/search/trailing-action.tsx +1 -1
  49. package/src/ui/shared/constants.ts +25 -27
  50. package/src/ui/shared/motion-tokens.ts +238 -0
  51. package/src/ui/snackbar/snackbar.tsx +4 -6
  52. package/src/ui/switch/switch.tsx +12 -18
  53. package/src/ui/text-field/text-field.tokens.ts +12 -12
  54. package/src/ui/text-field/text-field.tsx +31 -19
  55. package/src/ui/theme-provider/index.tsx +1 -5
  56. package/src/ui/toc.tsx +1 -1
  57. package/src/ui/toolbar/__snapshots__/bottom-docked-toolbar.test.tsx.snap +51 -0
  58. package/src/ui/toolbar/__snapshots__/floating-toolbar-with-fab.test.tsx.snap +113 -0
  59. package/src/ui/toolbar/__snapshots__/floating-toolbar.test.tsx.snap +169 -0
  60. package/src/ui/toolbar/bottom-docked-toolbar.test.tsx +114 -0
  61. package/src/ui/toolbar/docked-toolbar.tsx +186 -0
  62. package/src/ui/toolbar/floating-toolbar-with-fab.test.tsx +139 -0
  63. package/src/ui/toolbar/floating-toolbar-with-fab.tsx +199 -0
  64. package/src/ui/toolbar/floating-toolbar.test.tsx +230 -0
  65. package/src/ui/toolbar/floating-toolbar.tsx +344 -0
  66. package/src/ui/toolbar/index.ts +35 -0
  67. package/src/ui/toolbar/toolbar-colors.ts +37 -0
  68. package/src/ui/toolbar/toolbar-context.tsx +13 -0
  69. package/src/ui/toolbar/toolbar-divider.test.tsx +54 -0
  70. package/src/ui/toolbar/toolbar-divider.tsx +73 -0
  71. package/src/ui/toolbar/toolbar-icon-button.test.tsx +68 -0
  72. package/src/ui/toolbar/toolbar-icon-button.tsx +136 -0
  73. package/src/ui/toolbar/toolbar-scroll-behavior.ts +140 -0
  74. package/src/ui/toolbar/toolbar-tokens.ts +51 -0
  75. package/test-clip.html +31 -0
  76. package/test-shadow.html +5 -1
  77. package/test-width.html +34 -0
  78. package/src/ui/button-group.tsx +0 -350
  79. package/src/ui/button.tsx +0 -665
  80. package/test-render.tsx +0 -4
  81. package/test_output.txt +0 -164
  82. package/test_output_v2.txt +0 -5
  83. /package/src/ui/{fab.test.tsx → buttons/fabs/fab/fab.test.tsx} +0 -0
  84. /package/src/ui/{fab-menu.test.tsx → buttons/fabs/fab-menu/fab-menu.test.tsx} +0 -0
  85. /package/src/ui/{icon-button.test.tsx → buttons/icon-button/icon-button.test.tsx} +0 -0
  86. /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 "../lib/utils";
17
- import { LoadingIndicator } from "./loading-indicator";
18
- import { ProgressIndicator } from "./progress-indicator";
19
- import { Ripple, useRippleState } from "./ripple";
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 "./shared/constants";
25
- import { TouchTarget } from "./shared/touch-target";
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 "../lib/utils";
24
- import { Ripple, useRippleState } from "./ripple";
25
- import { SPRING_TRANSITION, SPRING_TRANSITION_FAST } from "./shared/constants";
26
- import { TouchTarget } from "./shared/touch-target";
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";
@@ -0,0 +1,2 @@
1
+ export * from "./fab";
2
+ 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 "../lib/utils";
17
- import { LoadingIndicator } from "./loading-indicator";
18
- import { ProgressIndicator } from "./progress-indicator";
19
- import { Ripple, useRippleState } from "./ripple";
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 "./shared/constants";
25
- import { TouchTarget } from "./shared/touch-target";
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";
@@ -0,0 +1,4 @@
1
+ export * from "./button";
2
+ export * from "./button-group";
3
+ export * from "./fabs";
4
+ export * from "./icon-button";
@@ -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
- const MD3_SPRING = {
23
- type: "spring" as const,
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 từ dưới lên, spring physics giống Google Material's "Emphasized" easing
9
- const MD3_DRAWER_SPRING = {
10
- type: "spring" as const,
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: mirrors MotionSchemeKeyTokens.FastSpatial
3
- // Android: spring(stiffness=380, dampingRatio=0.7)
4
- // Framer: damping = 2 × 0.7 × √(380 × 1) 27.3 → use 28
5
- // FastEffects: mirrors MotionSchemeKeyTokens.FastEffects
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 { Transition, Variants } from "motion/react";
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
- // ─── Shared spring/easing specs ───────────────────────────────────────────────
13
+ export { FAST_EFFECTS_SPRING, FAST_SPATIAL_SPRING };
12
14
 
13
- /** FastSpatial spring — used for shape morphing and spatial enter animations */
14
- export const FAST_SPATIAL_SPRING: Transition = {
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.4, 0, 1, 1], // FastOutLinearIn
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: "16px",
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: "16px 16px 8px 8px",
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: "4px",
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: "8px 8px 16px 16px",
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: "4px",
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
- it("FAST_SPATIAL_SPRING has correct spring parameters", () => {
621
- expect(FAST_SPATIAL_SPRING.stiffness).toBe(380);
622
- expect(FAST_SPATIAL_SPRING.damping).toBe(28);
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
+ });