@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,139 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import * as React from "react";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ HorizontalFloatingToolbarWithFab,
6
+ VerticalFloatingToolbarWithFab,
7
+ } from "./floating-toolbar-with-fab";
8
+
9
+ describe("FloatingToolbarWithFab", () => {
10
+ it("renders toolbar with FAB at end position (default)", () => {
11
+ render(
12
+ <HorizontalFloatingToolbarWithFab
13
+ expanded={true}
14
+ floatingActionButton={
15
+ <button type="button" data-testid="fab">
16
+ FAB
17
+ </button>
18
+ }
19
+ >
20
+ <button type="button">Center</button>
21
+ </HorizontalFloatingToolbarWithFab>,
22
+ );
23
+ const fab = screen.getByTestId("fab");
24
+ expect(fab).toBeInTheDocument();
25
+ });
26
+
27
+ it("renders toolbar with FAB at start position", () => {
28
+ render(
29
+ <HorizontalFloatingToolbarWithFab
30
+ expanded={true}
31
+ fabPosition="start"
32
+ floatingActionButton={
33
+ <button type="button" data-testid="fab">
34
+ FAB
35
+ </button>
36
+ }
37
+ >
38
+ <button type="button">Center</button>
39
+ </HorizontalFloatingToolbarWithFab>,
40
+ );
41
+ expect(screen.getByTestId("fab")).toBeInTheDocument();
42
+ });
43
+
44
+ it("toolbar and FAB maintain 8px gap via gap property", () => {
45
+ const { container } = render(
46
+ <HorizontalFloatingToolbarWithFab
47
+ expanded={true}
48
+ floatingActionButton={<button type="button">FAB</button>}
49
+ >
50
+ <button type="button">Center</button>
51
+ </HorizontalFloatingToolbarWithFab>,
52
+ );
53
+ // Gap is now applied as a style on the parent m.div
54
+ const innerFlex = container.firstChild?.firstChild as HTMLElement;
55
+ expect(innerFlex).toHaveStyle({ gap: "8px" });
56
+ });
57
+
58
+ it("standard and vibrant color variants (via colors prop)", () => {
59
+ const vibrantColors = {
60
+ toolbarContainerColor: "var(--md-sys-color-primary-container)",
61
+ toolbarContentColor: "var(--md-sys-color-on-primary-container)",
62
+ };
63
+ const { container } = render(
64
+ <HorizontalFloatingToolbarWithFab
65
+ expanded={true}
66
+ colors={vibrantColors}
67
+ floatingActionButton={<button type="button">FAB</button>}
68
+ >
69
+ <button type="button">Center</button>
70
+ </HorizontalFloatingToolbarWithFab>,
71
+ );
72
+ // It passes colors to the child Toolbar
73
+ expect(container.firstChild).toBeInTheDocument();
74
+ });
75
+
76
+ it("expanded/collapsed states resize the FAB container", () => {
77
+ // expanded=true size=80, expanded=false size=56
78
+ const { rerender, container } = render(
79
+ <HorizontalFloatingToolbarWithFab
80
+ expanded={false}
81
+ floatingActionButton={<button type="button">FAB</button>}
82
+ >
83
+ <button type="button">Center</button>
84
+ </HorizontalFloatingToolbarWithFab>,
85
+ );
86
+
87
+ // We cannot reliably assert motion inline styles instantly in standard jsdom without timers,
88
+ // but we know it doesn't throw.
89
+ expect(container.firstChild).toBeInTheDocument();
90
+
91
+ rerender(
92
+ <HorizontalFloatingToolbarWithFab
93
+ expanded={true}
94
+ floatingActionButton={<button type="button">FAB</button>}
95
+ >
96
+ <button type="button">Center</button>
97
+ </HorizontalFloatingToolbarWithFab>,
98
+ );
99
+ expect(container.firstChild).toBeInTheDocument();
100
+ });
101
+
102
+ it("forwards ref", () => {
103
+ const ref = React.createRef<HTMLDivElement>();
104
+ render(
105
+ <HorizontalFloatingToolbarWithFab
106
+ expanded={true}
107
+ ref={ref}
108
+ floatingActionButton={<button type="button">FAB</button>}
109
+ >
110
+ <button type="button">Center</button>
111
+ </HorizontalFloatingToolbarWithFab>,
112
+ );
113
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
114
+ });
115
+
116
+ it("snapshots: horizontal with fab end", () => {
117
+ const { container } = render(
118
+ <HorizontalFloatingToolbarWithFab
119
+ expanded={true}
120
+ floatingActionButton={<button type="button">FAB</button>}
121
+ >
122
+ <button type="button">Center</button>
123
+ </HorizontalFloatingToolbarWithFab>,
124
+ );
125
+ expect(container.firstChild).toMatchSnapshot();
126
+ });
127
+
128
+ it("snapshots: vertical with fab bottom", () => {
129
+ const { container } = render(
130
+ <VerticalFloatingToolbarWithFab
131
+ expanded={true}
132
+ floatingActionButton={<button type="button">FAB</button>}
133
+ >
134
+ <button type="button">Center</button>
135
+ </VerticalFloatingToolbarWithFab>,
136
+ );
137
+ expect(container.firstChild).toMatchSnapshot();
138
+ });
139
+ });
@@ -0,0 +1,199 @@
1
+ import { AnimatePresence, m } from "motion/react";
2
+ import * as React from "react";
3
+ import { cn } from "../../lib/utils";
4
+ import { SPRING_TRANSITION } from "../shared/constants";
5
+ import {
6
+ type FloatingToolbarProps,
7
+ HorizontalFloatingToolbar,
8
+ VerticalFloatingToolbar,
9
+ } from "./floating-toolbar";
10
+ import { standardFloatingToolbarColors } from "./toolbar-colors";
11
+ import {
12
+ FabBaselineTokens,
13
+ FloatingToolbarTokens,
14
+ ToolbarToFabGap,
15
+ } from "./toolbar-tokens";
16
+
17
+ export interface FloatingToolbarWithFabProps extends FloatingToolbarProps {
18
+ /** FAB element (use FAB component or custom node) */
19
+ floatingActionButton: React.ReactNode;
20
+ /** FAB position for horizontal: 'start' | 'end'. For vertical: 'top' | 'bottom' */
21
+ fabPosition?: "start" | "end" | "top" | "bottom";
22
+ /** Animation duration override */
23
+ animationDuration?: number;
24
+ /** Start content (explicitly declared to resolve type issues) */
25
+ startContent?: React.ReactNode;
26
+ /** End content (explicitly declared to resolve type issues) */
27
+ endContent?: React.ReactNode;
28
+ /**
29
+ * Size of the FAB container.
30
+ * - `"default"` → 56×56px (`FabBaselineTokens.ContainerHeight`)
31
+ * - `"medium"` → 80×80px (`FabBaselineTokens.MediumContainerHeight`)
32
+ *
33
+ * MD3: pair a medium FAB with the toolbar to give it greater visual prominence.
34
+ * @default "default"
35
+ */
36
+ fabSize?: "default" | "medium";
37
+ }
38
+
39
+ const FloatingToolbarWithFabBase = React.forwardRef<
40
+ HTMLDivElement,
41
+ FloatingToolbarWithFabProps
42
+ >(
43
+ (
44
+ {
45
+ expanded,
46
+ orientation = "horizontal",
47
+ colors = standardFloatingToolbarColors,
48
+ floatingActionButton,
49
+ fabPosition = orientation === "horizontal" ? "end" : "bottom",
50
+ animationDuration = 0.3,
51
+ scrollBehavior,
52
+ fabSize = "default",
53
+ className,
54
+ style,
55
+ ...props
56
+ },
57
+ ref,
58
+ ) => {
59
+ const isHorizontal = orientation === "horizontal";
60
+
61
+ const effectiveExpanded =
62
+ expanded && (!scrollBehavior || scrollBehavior.isExpanded);
63
+
64
+ const isFabBefore = fabPosition === "start" || fabPosition === "top";
65
+
66
+ const resolvedFabSize =
67
+ fabSize === "medium"
68
+ ? FabBaselineTokens.MediumContainerHeight
69
+ : FabBaselineTokens.ContainerHeight;
70
+
71
+ const transitionSpec = {
72
+ ...SPRING_TRANSITION,
73
+ duration: animationDuration,
74
+ };
75
+
76
+ const SHADOW_PADDING = 24;
77
+
78
+ const fabContainerNode = (
79
+ <m.div
80
+ layout
81
+ key="fab-container"
82
+ style={{
83
+ width: `${resolvedFabSize}px`,
84
+ height: `${resolvedFabSize}px`,
85
+ }}
86
+ className={cn(
87
+ "flex shrink-0 items-center justify-center relative z-10",
88
+ "*:w-full *:h-full",
89
+ )}
90
+ >
91
+ {floatingActionButton}
92
+ </m.div>
93
+ );
94
+
95
+ return (
96
+ <div ref={ref} style={style}>
97
+ <m.div
98
+ layout
99
+ initial={false}
100
+ animate={{
101
+ gap: effectiveExpanded ? `${ToolbarToFabGap}px` : "0px",
102
+ }}
103
+ transition={transitionSpec}
104
+ className={cn(
105
+ "flex items-center justify-center pointer-events-auto",
106
+ isHorizontal
107
+ ? "flex-row h-(--toolbar-size)"
108
+ : "flex-col w-(--toolbar-size) h-fit",
109
+ className,
110
+ )}
111
+ style={
112
+ {
113
+ "--fab-size": `${resolvedFabSize}px`,
114
+ "--toolbar-size": `${FloatingToolbarTokens.ContainerHeight}px`,
115
+ } as React.CSSProperties
116
+ }
117
+ >
118
+ {isFabBefore && fabContainerNode}
119
+
120
+ <AnimatePresence initial={false}>
121
+ {effectiveExpanded && (
122
+ <m.div
123
+ key="toolbar-content-wrapper"
124
+ initial={{
125
+ opacity: 0,
126
+ scale: 0.9,
127
+ width: isHorizontal ? 0 : "auto",
128
+ height: !isHorizontal ? 0 : "auto",
129
+ }}
130
+ animate={{
131
+ opacity: 1,
132
+ scale: 1,
133
+ width: "auto",
134
+ height: "auto",
135
+ }}
136
+ exit={{
137
+ opacity: 0,
138
+ scale: 0.9,
139
+ width: isHorizontal ? 0 : "auto",
140
+ height: !isHorizontal ? 0 : "auto",
141
+ }}
142
+ transition={transitionSpec}
143
+ style={{
144
+ padding: SHADOW_PADDING,
145
+ margin: -SHADOW_PADDING,
146
+ }}
147
+ className="flex items-center shrink-0 min-w-0 min-h-0 overflow-hidden relative z-0"
148
+ >
149
+ {isHorizontal ? (
150
+ <HorizontalFloatingToolbar
151
+ expanded={expanded}
152
+ colors={colors}
153
+ disableScrollTranslation
154
+ disableLayoutAnimation
155
+ {...props}
156
+ />
157
+ ) : (
158
+ <VerticalFloatingToolbar
159
+ expanded={expanded}
160
+ colors={colors}
161
+ disableScrollTranslation
162
+ disableLayoutAnimation
163
+ {...props}
164
+ />
165
+ )}
166
+ </m.div>
167
+ )}
168
+ </AnimatePresence>
169
+
170
+ {!isFabBefore && fabContainerNode}
171
+ </m.div>
172
+ </div>
173
+ );
174
+ },
175
+ );
176
+ FloatingToolbarWithFabBase.displayName = "FloatingToolbarWithFabBase";
177
+
178
+ /**
179
+ * A horizontal floating toolbar paired with a FAB.
180
+ */
181
+ export const HorizontalFloatingToolbarWithFab = React.forwardRef<
182
+ HTMLDivElement,
183
+ FloatingToolbarWithFabProps
184
+ >((props, ref) => (
185
+ <FloatingToolbarWithFabBase ref={ref} orientation="horizontal" {...props} />
186
+ ));
187
+ HorizontalFloatingToolbarWithFab.displayName =
188
+ "HorizontalFloatingToolbarWithFab";
189
+
190
+ /**
191
+ * A vertical floating toolbar paired with a FAB.
192
+ */
193
+ export const VerticalFloatingToolbarWithFab = React.forwardRef<
194
+ HTMLDivElement,
195
+ FloatingToolbarWithFabProps
196
+ >((props, ref) => (
197
+ <FloatingToolbarWithFabBase ref={ref} orientation="vertical" {...props} />
198
+ ));
199
+ VerticalFloatingToolbarWithFab.displayName = "VerticalFloatingToolbarWithFab";
@@ -0,0 +1,230 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import * as React from "react";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import {
5
+ HorizontalFloatingToolbar,
6
+ VerticalFloatingToolbar,
7
+ } from "./floating-toolbar";
8
+ import { useFloatingToolbarScrollBehavior } from "./toolbar-scroll-behavior";
9
+
10
+ // Mock matchMedia for jsdom
11
+ Object.defineProperty(window, "matchMedia", {
12
+ writable: true,
13
+ value: vi.fn().mockImplementation((query) => ({
14
+ matches: false,
15
+ media: query,
16
+ onchange: null,
17
+ addListener: vi.fn(), // Deprecated
18
+ removeListener: vi.fn(), // Deprecated
19
+ addEventListener: vi.fn(),
20
+ removeEventListener: vi.fn(),
21
+ dispatchEvent: vi.fn(),
22
+ })),
23
+ });
24
+
25
+ describe("HorizontalFloatingToolbar", () => {
26
+ it('renders horizontal variant with correct role="toolbar"', () => {
27
+ render(
28
+ <HorizontalFloatingToolbar expanded={true}>
29
+ <button type="button">Test</button>
30
+ </HorizontalFloatingToolbar>,
31
+ );
32
+ expect(screen.getByRole("toolbar")).toBeInTheDocument();
33
+ });
34
+
35
+ it("shows startContent when expanded=true", () => {
36
+ render(
37
+ <HorizontalFloatingToolbar
38
+ expanded={true}
39
+ startContent={<span data-testid="leading" />}
40
+ >
41
+ <button type="button">Test</button>
42
+ </HorizontalFloatingToolbar>,
43
+ );
44
+ expect(screen.getByTestId("leading")).toBeInTheDocument();
45
+ });
46
+
47
+ it("hides startContent when expanded=false", () => {
48
+ render(
49
+ <HorizontalFloatingToolbar
50
+ expanded={false}
51
+ startContent={<span data-testid="leading" />}
52
+ >
53
+ <button type="button">Test</button>
54
+ </HorizontalFloatingToolbar>,
55
+ );
56
+ // Framer motion with AnimatePresence might still have it briefly during exit, but initial=false means it shouldn't render at all
57
+ expect(screen.queryByTestId("leading")).not.toBeInTheDocument();
58
+ });
59
+
60
+ it("shows endContent when expanded=true", () => {
61
+ render(
62
+ <HorizontalFloatingToolbar
63
+ expanded={true}
64
+ endContent={<span data-testid="trailing" />}
65
+ >
66
+ <button type="button">Test</button>
67
+ </HorizontalFloatingToolbar>,
68
+ );
69
+ expect(screen.getByTestId("trailing")).toBeInTheDocument();
70
+ });
71
+
72
+ it("applies standard colors by default", () => {
73
+ render(
74
+ <HorizontalFloatingToolbar expanded={true}>
75
+ <button type="button">Test</button>
76
+ </HorizontalFloatingToolbar>,
77
+ );
78
+ // standard colors map to surface-container
79
+ expect(screen.getByRole("toolbar")).toHaveStyle({
80
+ backgroundColor: "var(--toolbar-bg)",
81
+ });
82
+ });
83
+
84
+ it("applies vibrant colors when custom colors are passed", () => {
85
+ const vibrantColors = {
86
+ toolbarContainerColor: "var(--md-sys-color-primary-container)",
87
+ toolbarContentColor: "var(--md-sys-color-on-primary-container)",
88
+ };
89
+ render(
90
+ <HorizontalFloatingToolbar expanded={true} colors={vibrantColors}>
91
+ <button type="button">Test</button>
92
+ </HorizontalFloatingToolbar>,
93
+ );
94
+ const style = screen.getByRole("toolbar").style;
95
+ expect(style.getPropertyValue("--toolbar-bg")).toBe(
96
+ "var(--md-sys-color-primary-container)",
97
+ );
98
+ });
99
+
100
+ it("applies custom className", () => {
101
+ render(
102
+ <HorizontalFloatingToolbar expanded={true} className="custom-class">
103
+ <button type="button">Test</button>
104
+ </HorizontalFloatingToolbar>,
105
+ );
106
+ expect(screen.getByRole("toolbar")).toHaveClass("custom-class");
107
+ });
108
+
109
+ it("forwards ref to container element", () => {
110
+ const ref = React.createRef<HTMLDivElement>();
111
+ render(
112
+ <HorizontalFloatingToolbar expanded={true} ref={ref}>
113
+ <button type="button">Test</button>
114
+ </HorizontalFloatingToolbar>,
115
+ );
116
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
117
+ });
118
+
119
+ it("aria-label is applied to toolbar container", () => {
120
+ render(
121
+ <HorizontalFloatingToolbar expanded={true} aria-label="Test Label">
122
+ <button type="button">Test</button>
123
+ </HorizontalFloatingToolbar>,
124
+ );
125
+ expect(screen.getByRole("toolbar")).toHaveAttribute(
126
+ "aria-label",
127
+ "Test Label",
128
+ );
129
+ });
130
+
131
+ it("snapshots: horizontal expanded", () => {
132
+ const { container } = render(
133
+ <HorizontalFloatingToolbar
134
+ expanded={true}
135
+ startContent={<span>Lead</span>}
136
+ endContent={<span>Trail</span>}
137
+ >
138
+ <button type="button">Center</button>
139
+ </HorizontalFloatingToolbar>,
140
+ );
141
+ expect(container.firstChild).toMatchSnapshot();
142
+ });
143
+
144
+ it("snapshots: horizontal collapsed", () => {
145
+ const { container } = render(
146
+ <HorizontalFloatingToolbar
147
+ expanded={false}
148
+ startContent={<span>Lead</span>}
149
+ endContent={<span>Trail</span>}
150
+ >
151
+ <button type="button">Center</button>
152
+ </HorizontalFloatingToolbar>,
153
+ );
154
+ expect(container.firstChild).toMatchSnapshot();
155
+ });
156
+ });
157
+
158
+ describe("VerticalFloatingToolbar", () => {
159
+ it("renders vertical variant", () => {
160
+ render(
161
+ <VerticalFloatingToolbar expanded={true}>
162
+ <button type="button">Test</button>
163
+ </VerticalFloatingToolbar>,
164
+ );
165
+ expect(screen.getByRole("toolbar")).toHaveAttribute(
166
+ "aria-orientation",
167
+ "vertical",
168
+ );
169
+ });
170
+
171
+ it("snapshots: vertical expanded", () => {
172
+ const { container } = render(
173
+ <VerticalFloatingToolbar
174
+ expanded={true}
175
+ startContent={<span>Lead</span>}
176
+ endContent={<span>Trail</span>}
177
+ >
178
+ <button type="button">Center</button>
179
+ </VerticalFloatingToolbar>,
180
+ );
181
+ expect(container.firstChild).toMatchSnapshot();
182
+ });
183
+
184
+ it("snapshots: vertical collapsed", () => {
185
+ const { container } = render(
186
+ <VerticalFloatingToolbar
187
+ expanded={false}
188
+ startContent={<span>Lead</span>}
189
+ endContent={<span>Trail</span>}
190
+ >
191
+ <button type="button">Center</button>
192
+ </VerticalFloatingToolbar>,
193
+ );
194
+ expect(container.firstChild).toMatchSnapshot();
195
+ });
196
+ });
197
+
198
+ describe("useFloatingToolbarScrollBehavior", () => {
199
+ it("calls onScroll handler when scrollBehavior is provided", () => {
200
+ const TestComponent = () => {
201
+ const scrollBehavior = useFloatingToolbarScrollBehavior({
202
+ collapseThreshold: 0,
203
+ expandThreshold: 0,
204
+ });
205
+ return (
206
+ <div
207
+ data-testid="scrollable"
208
+ onScroll={scrollBehavior.onScroll}
209
+ style={{ height: 100, overflow: "auto" }}
210
+ >
211
+ <div style={{ height: 500 }}>
212
+ <HorizontalFloatingToolbar
213
+ expanded={scrollBehavior.isExpanded}
214
+ scrollBehavior={scrollBehavior}
215
+ >
216
+ <button type="button">Test</button>
217
+ </HorizontalFloatingToolbar>
218
+ </div>
219
+ </div>
220
+ );
221
+ };
222
+
223
+ render(<TestComponent />);
224
+ const scrollable = screen.getByTestId("scrollable");
225
+
226
+ fireEvent.scroll(scrollable, { target: { scrollTop: 50 } });
227
+ // Without full DOM we can't easily test the exact behavior of the hook state visually
228
+ // but we check it doesn't crash
229
+ });
230
+ });