@zentauri-ui/zentauri-components 2.1.7 → 2.1.9
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/README.md +8 -5
- package/cli/props.json +350 -3
- package/cli/registry.json +11 -0
- package/dist/{chunk-DBNGLT5U.mjs → chunk-3HBC34NF.mjs} +4 -4
- package/dist/{chunk-DBNGLT5U.mjs.map → chunk-3HBC34NF.mjs.map} +1 -1
- package/dist/{chunk-FUCW5GPE.mjs → chunk-3RC5IG6O.mjs} +55 -8
- package/dist/chunk-3RC5IG6O.mjs.map +1 -0
- package/dist/chunk-4SLVTSHM.js +241 -0
- package/dist/chunk-4SLVTSHM.js.map +1 -0
- package/dist/{chunk-TJ2EWPER.js → chunk-4TPE5DEG.js} +63 -7
- package/dist/chunk-4TPE5DEG.js.map +1 -0
- package/dist/{chunk-G7FVHZRB.js → chunk-7CZDJTPD.js} +12 -12
- package/dist/{chunk-G7FVHZRB.js.map → chunk-7CZDJTPD.js.map} +1 -1
- package/dist/{chunk-5ELR6MIN.js → chunk-7DGPRPWM.js} +6 -6
- package/dist/{chunk-5ELR6MIN.js.map → chunk-7DGPRPWM.js.map} +1 -1
- package/dist/chunk-GP3FUS2H.mjs +26 -0
- package/dist/chunk-GP3FUS2H.mjs.map +1 -0
- package/dist/chunk-IHDM7AHY.mjs +233 -0
- package/dist/chunk-IHDM7AHY.mjs.map +1 -0
- package/dist/chunk-MWG7LHAK.js +19 -0
- package/dist/{chunk-5FU57ZVQ.js.map → chunk-MWG7LHAK.js.map} +1 -1
- package/dist/{chunk-7UXPXCKV.mjs → chunk-OLT7P7JO.mjs} +3 -3
- package/dist/{chunk-7UXPXCKV.mjs.map → chunk-OLT7P7JO.mjs.map} +1 -1
- package/dist/chunk-PAISX7YL.js +38 -0
- package/dist/chunk-PAISX7YL.js.map +1 -0
- package/dist/{chunk-KVSRUAXP.mjs → chunk-VN7FE5RR.mjs} +3 -3
- package/dist/{chunk-KVSRUAXP.mjs.map → chunk-VN7FE5RR.mjs.map} +1 -1
- package/dist/design-system/code-diff.d.ts +18 -0
- package/dist/design-system/code-diff.d.ts.map +1 -0
- package/dist/design-system/facade.js +8 -7
- package/dist/design-system/facade.js.map +1 -1
- package/dist/design-system/facade.mjs +7 -6
- package/dist/design-system/facade.mjs.map +1 -1
- package/dist/design-system/index.d.ts +2 -0
- package/dist/design-system/index.d.ts.map +1 -1
- package/dist/design-system/split-button.d.ts +25 -0
- package/dist/design-system/split-button.d.ts.map +1 -0
- package/dist/ui/buttons/animated.js +10 -9
- package/dist/ui/buttons/animated.js.map +1 -1
- package/dist/ui/buttons/animated.mjs +8 -7
- package/dist/ui/buttons/animated.mjs.map +1 -1
- package/dist/ui/buttons.js +11 -10
- package/dist/ui/buttons.mjs +9 -8
- package/dist/ui/code-diff/code-diff-base.d.ts +6 -0
- package/dist/ui/code-diff/code-diff-base.d.ts.map +1 -0
- package/dist/ui/code-diff/code-diff.d.ts +6 -0
- package/dist/ui/code-diff/code-diff.d.ts.map +1 -0
- package/dist/ui/code-diff/index.d.ts +4 -0
- package/dist/ui/code-diff/index.d.ts.map +1 -0
- package/dist/ui/code-diff/types.d.ts +26 -0
- package/dist/ui/code-diff/types.d.ts.map +1 -0
- package/dist/ui/code-diff/variants.d.ts +11 -0
- package/dist/ui/code-diff/variants.d.ts.map +1 -0
- package/dist/ui/code-diff.js +302 -0
- package/dist/ui/code-diff.js.map +1 -0
- package/dist/ui/code-diff.mjs +297 -0
- package/dist/ui/code-diff.mjs.map +1 -0
- package/dist/ui/data-table.js +22 -21
- package/dist/ui/data-table.js.map +1 -1
- package/dist/ui/data-table.mjs +12 -11
- package/dist/ui/data-table.mjs.map +1 -1
- package/dist/ui/dropdown/dropdown.d.ts +1 -1
- package/dist/ui/dropdown/dropdown.d.ts.map +1 -1
- package/dist/ui/dropdown/types.d.ts +2 -2
- package/dist/ui/dropdown/types.d.ts.map +1 -1
- package/dist/ui/dropdown.js +31 -231
- package/dist/ui/dropdown.js.map +1 -1
- package/dist/ui/dropdown.mjs +4 -229
- package/dist/ui/dropdown.mjs.map +1 -1
- package/dist/ui/dynamic-stepper.js +20 -19
- package/dist/ui/dynamic-stepper.js.map +1 -1
- package/dist/ui/dynamic-stepper.mjs +9 -8
- package/dist/ui/dynamic-stepper.mjs.map +1 -1
- package/dist/ui/pagination.js +12 -11
- package/dist/ui/pagination.mjs +9 -8
- package/dist/ui/split-button/index.d.ts +4 -0
- package/dist/ui/split-button/index.d.ts.map +1 -0
- package/dist/ui/split-button/split-button-base.d.ts +6 -0
- package/dist/ui/split-button/split-button-base.d.ts.map +1 -0
- package/dist/ui/split-button/split-button.d.ts +6 -0
- package/dist/ui/split-button/split-button.d.ts.map +1 -0
- package/dist/ui/split-button/types.d.ts +30 -0
- package/dist/ui/split-button/types.d.ts.map +1 -0
- package/dist/ui/split-button/variants.d.ts +16 -0
- package/dist/ui/split-button/variants.d.ts.map +1 -0
- package/dist/ui/split-button.js +288 -0
- package/dist/ui/split-button.js.map +1 -0
- package/dist/ui/split-button.mjs +279 -0
- package/dist/ui/split-button.mjs.map +1 -0
- package/package.json +4 -1
- package/src/design-system/code-diff.ts +37 -0
- package/src/design-system/index.ts +2 -0
- package/src/design-system/split-button.ts +38 -0
- package/src/ui/code-diff/code-diff-base.tsx +284 -0
- package/src/ui/code-diff/code-diff.test.tsx +50 -0
- package/src/ui/code-diff/code-diff.tsx +8 -0
- package/src/ui/code-diff/index.ts +15 -0
- package/src/ui/code-diff/types.ts +31 -0
- package/src/ui/code-diff/variants.ts +49 -0
- package/src/ui/dropdown/dropdown.tsx +7 -3
- package/src/ui/dropdown/types.ts +2 -2
- package/src/ui/split-button/index.ts +19 -0
- package/src/ui/split-button/split-button-base.tsx +232 -0
- package/src/ui/split-button/split-button.test.tsx +208 -0
- package/src/ui/split-button/split-button.tsx +9 -0
- package/src/ui/split-button/types.ts +46 -0
- package/src/ui/split-button/variants.ts +46 -0
- package/dist/chunk-5FU57ZVQ.js +0 -19
- package/dist/chunk-FUCW5GPE.mjs.map +0 -1
- package/dist/chunk-TJ2EWPER.js.map +0 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { FiChevronDown, FiLoader } from "react-icons/fi";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Dropdown,
|
|
8
|
+
DropdownContent,
|
|
9
|
+
DropdownItem,
|
|
10
|
+
DropdownTrigger,
|
|
11
|
+
} from "../dropdown";
|
|
12
|
+
import { buttonVariants } from "../buttons";
|
|
13
|
+
import { cn } from "../../lib/utils";
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
SplitButtonAppearance,
|
|
17
|
+
SplitButtonProps,
|
|
18
|
+
SplitButtonVariant,
|
|
19
|
+
} from "./types";
|
|
20
|
+
import {
|
|
21
|
+
splitButtonContentVariants,
|
|
22
|
+
splitButtonDropdownVariants,
|
|
23
|
+
splitButtonGroupVariants,
|
|
24
|
+
splitButtonItemDisabledVariants,
|
|
25
|
+
splitButtonPrimaryVariants,
|
|
26
|
+
splitButtonRootVariants,
|
|
27
|
+
splitButtonTriggerVariants,
|
|
28
|
+
} from "./variants";
|
|
29
|
+
|
|
30
|
+
const variantAppearanceMap = {
|
|
31
|
+
primary: "default",
|
|
32
|
+
secondary: "secondary",
|
|
33
|
+
outline: "outline",
|
|
34
|
+
ghost: "ghost",
|
|
35
|
+
danger: "destructive",
|
|
36
|
+
success: "green",
|
|
37
|
+
} as const satisfies Record<SplitButtonVariant, SplitButtonAppearance>;
|
|
38
|
+
|
|
39
|
+
function resolveAppearance({
|
|
40
|
+
appearance,
|
|
41
|
+
variant,
|
|
42
|
+
}: {
|
|
43
|
+
appearance?: SplitButtonAppearance;
|
|
44
|
+
variant?: SplitButtonVariant;
|
|
45
|
+
}) {
|
|
46
|
+
return appearance ?? (variant ? variantAppearanceMap[variant] : "default");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function SplitButtonBase({
|
|
50
|
+
label,
|
|
51
|
+
onClick,
|
|
52
|
+
items,
|
|
53
|
+
disabled = false,
|
|
54
|
+
loading = false,
|
|
55
|
+
appearance,
|
|
56
|
+
variant,
|
|
57
|
+
size = "md",
|
|
58
|
+
startIcon,
|
|
59
|
+
fullWidth = false,
|
|
60
|
+
open: controlledOpen,
|
|
61
|
+
defaultOpen = false,
|
|
62
|
+
onOpenChange,
|
|
63
|
+
triggerLabel,
|
|
64
|
+
triggerOn = "click",
|
|
65
|
+
className,
|
|
66
|
+
ref,
|
|
67
|
+
...rest
|
|
68
|
+
}: SplitButtonProps) {
|
|
69
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
70
|
+
const isUnavailable = disabled || loading;
|
|
71
|
+
const resolvedAppearance = resolveAppearance({ appearance, variant });
|
|
72
|
+
const isControlled = controlledOpen !== undefined;
|
|
73
|
+
const open = isUnavailable
|
|
74
|
+
? false
|
|
75
|
+
: isControlled
|
|
76
|
+
? controlledOpen
|
|
77
|
+
: uncontrolledOpen;
|
|
78
|
+
|
|
79
|
+
const setOpen = (nextOpen: boolean) => {
|
|
80
|
+
if (isUnavailable && nextOpen) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!isControlled) {
|
|
84
|
+
setUncontrolledOpen(nextOpen);
|
|
85
|
+
}
|
|
86
|
+
onOpenChange?.(nextOpen);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const fullWidthFlag = fullWidth ? "true" : undefined;
|
|
90
|
+
const menuLabel = triggerLabel ?? `More ${label} actions`;
|
|
91
|
+
|
|
92
|
+
// Shared timeout ref for hover mode: delays close so the cursor can
|
|
93
|
+
// travel through the mt-2 gap between the button and the menu panel
|
|
94
|
+
// without dismissing the menu.
|
|
95
|
+
const hoverCloseRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
96
|
+
|
|
97
|
+
const scheduleClose = () => {
|
|
98
|
+
cancelClose();
|
|
99
|
+
hoverCloseRef.current = setTimeout(() => setOpen(false), 120);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const cancelClose = () => {
|
|
103
|
+
if (hoverCloseRef.current !== null) {
|
|
104
|
+
clearTimeout(hoverCloseRef.current);
|
|
105
|
+
hoverCloseRef.current = null;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
useEffect(() => cancelClose, []);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (isUnavailable && !isControlled) {
|
|
113
|
+
setUncontrolledOpen(false);
|
|
114
|
+
onOpenChange?.(false);
|
|
115
|
+
}
|
|
116
|
+
}, [isUnavailable, onOpenChange, isControlled]);
|
|
117
|
+
|
|
118
|
+
const dropdownHoverHandlers =
|
|
119
|
+
triggerOn === "hover"
|
|
120
|
+
? {
|
|
121
|
+
onMouseEnter: () => {
|
|
122
|
+
cancelClose();
|
|
123
|
+
setOpen(true);
|
|
124
|
+
},
|
|
125
|
+
onMouseLeave: scheduleClose,
|
|
126
|
+
}
|
|
127
|
+
: undefined;
|
|
128
|
+
|
|
129
|
+
const contentHoverHandlers =
|
|
130
|
+
triggerOn === "hover"
|
|
131
|
+
? { onMouseEnter: cancelClose, onMouseLeave: scheduleClose }
|
|
132
|
+
: undefined;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
ref={ref}
|
|
137
|
+
data-slot="split-button"
|
|
138
|
+
data-full-width={fullWidthFlag}
|
|
139
|
+
className={cn(splitButtonRootVariants({ fullWidth }), className)}
|
|
140
|
+
{...rest}
|
|
141
|
+
>
|
|
142
|
+
<Dropdown
|
|
143
|
+
open={open}
|
|
144
|
+
defaultOpen={defaultOpen}
|
|
145
|
+
onOpenChange={setOpen}
|
|
146
|
+
data-full-width={fullWidthFlag}
|
|
147
|
+
className={splitButtonDropdownVariants({ fullWidth })}
|
|
148
|
+
{...dropdownHoverHandlers}
|
|
149
|
+
>
|
|
150
|
+
<div
|
|
151
|
+
data-slot="split-button-group"
|
|
152
|
+
data-full-width={fullWidthFlag}
|
|
153
|
+
className={splitButtonGroupVariants({ fullWidth })}
|
|
154
|
+
>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
data-slot="split-button-primary"
|
|
158
|
+
data-full-width={fullWidthFlag}
|
|
159
|
+
disabled={isUnavailable}
|
|
160
|
+
onClick={onClick}
|
|
161
|
+
className={cn(
|
|
162
|
+
buttonVariants({ appearance: resolvedAppearance, size }),
|
|
163
|
+
splitButtonPrimaryVariants(),
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
{loading ? (
|
|
167
|
+
<FiLoader
|
|
168
|
+
aria-hidden
|
|
169
|
+
className="animate-spin"
|
|
170
|
+
data-slot="split-button-spinner-icon"
|
|
171
|
+
/>
|
|
172
|
+
) : (
|
|
173
|
+
startIcon
|
|
174
|
+
)}
|
|
175
|
+
<span>{label}</span>
|
|
176
|
+
{loading ? (
|
|
177
|
+
<span className="sr-only" role="status" aria-label="Loading" />
|
|
178
|
+
) : null}
|
|
179
|
+
</button>
|
|
180
|
+
|
|
181
|
+
<DropdownTrigger
|
|
182
|
+
aria-label={menuLabel}
|
|
183
|
+
disabled={isUnavailable}
|
|
184
|
+
className={cn(
|
|
185
|
+
buttonVariants({ appearance: resolvedAppearance, size }),
|
|
186
|
+
splitButtonTriggerVariants({ size }),
|
|
187
|
+
)}
|
|
188
|
+
>
|
|
189
|
+
<FiChevronDown aria-hidden />
|
|
190
|
+
</DropdownTrigger>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<DropdownContent
|
|
194
|
+
className={splitButtonContentVariants()}
|
|
195
|
+
placement="bottom"
|
|
196
|
+
{...contentHoverHandlers}
|
|
197
|
+
>
|
|
198
|
+
{items.map((item) => (
|
|
199
|
+
<DropdownItem
|
|
200
|
+
key={item.id}
|
|
201
|
+
leftIcon={item.icon}
|
|
202
|
+
aria-disabled={item.disabled ? "true" : undefined}
|
|
203
|
+
className={
|
|
204
|
+
item.disabled ? splitButtonItemDisabledVariants() : undefined
|
|
205
|
+
}
|
|
206
|
+
onClick={(event) => {
|
|
207
|
+
if (item.disabled) {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
event.stopPropagation();
|
|
210
|
+
}
|
|
211
|
+
}}
|
|
212
|
+
onKeyDown={(event) => {
|
|
213
|
+
if (item.disabled) {
|
|
214
|
+
event.preventDefault();
|
|
215
|
+
event.stopPropagation();
|
|
216
|
+
}
|
|
217
|
+
}}
|
|
218
|
+
onSelect={() => {
|
|
219
|
+
setOpen(false);
|
|
220
|
+
item.onSelect?.();
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
{item.label}
|
|
224
|
+
</DropdownItem>
|
|
225
|
+
))}
|
|
226
|
+
</DropdownContent>
|
|
227
|
+
</Dropdown>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
SplitButtonBase.displayName = "SplitButton";
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { SplitButton } from "./split-button";
|
|
7
|
+
|
|
8
|
+
const items = [
|
|
9
|
+
{ id: "save-as", label: "Save As", onSelect: vi.fn() },
|
|
10
|
+
{ id: "export", label: "Export", onSelect: vi.fn() },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
describe("SplitButton", () => {
|
|
14
|
+
it("should render the primary label and menu trigger", () => {
|
|
15
|
+
render(<SplitButton label="Save" items={items} />);
|
|
16
|
+
|
|
17
|
+
expect(screen.getByRole("button", { name: "Save" })).toBeInTheDocument();
|
|
18
|
+
expect(
|
|
19
|
+
screen.getByRole("button", { name: /more save actions/i }),
|
|
20
|
+
).toHaveAttribute("aria-haspopup", "menu");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should call onClick from the primary button", async () => {
|
|
24
|
+
const user = userEvent.setup();
|
|
25
|
+
const onClick = vi.fn();
|
|
26
|
+
render(<SplitButton label="Save" items={items} onClick={onClick} />);
|
|
27
|
+
|
|
28
|
+
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
29
|
+
|
|
30
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should open the menu and call an item onSelect", async () => {
|
|
34
|
+
const user = userEvent.setup();
|
|
35
|
+
const handleExport = vi.fn();
|
|
36
|
+
render(
|
|
37
|
+
<SplitButton
|
|
38
|
+
label="Save"
|
|
39
|
+
items={[
|
|
40
|
+
{ id: "save-as", label: "Save As" },
|
|
41
|
+
{ id: "export", label: "Export", onSelect: handleExport },
|
|
42
|
+
]}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
await user.click(
|
|
47
|
+
screen.getByRole("button", { name: /more save actions/i }),
|
|
48
|
+
);
|
|
49
|
+
await user.click(await screen.findByRole("menuitem", { name: "Export" }));
|
|
50
|
+
|
|
51
|
+
expect(handleExport).toHaveBeenCalledTimes(1);
|
|
52
|
+
await waitFor(() => {
|
|
53
|
+
expect(screen.queryByRole("menuitem", { name: "Export" })).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should not open or call actions while disabled", async () => {
|
|
58
|
+
const user = userEvent.setup();
|
|
59
|
+
const onClick = vi.fn();
|
|
60
|
+
render(
|
|
61
|
+
<SplitButton
|
|
62
|
+
disabled
|
|
63
|
+
label="Save"
|
|
64
|
+
items={[{ id: "export", label: "Export" }]}
|
|
65
|
+
onClick={onClick}
|
|
66
|
+
/>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const buttons = screen.getAllByRole("button");
|
|
70
|
+
expect(buttons).toHaveLength(2);
|
|
71
|
+
const [primary, trigger] = buttons as [HTMLElement, HTMLElement];
|
|
72
|
+
|
|
73
|
+
expect(primary).toBeDisabled();
|
|
74
|
+
expect(trigger).toBeDisabled();
|
|
75
|
+
await user.click(primary);
|
|
76
|
+
await user.click(trigger);
|
|
77
|
+
|
|
78
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
79
|
+
expect(screen.queryByRole("menu")).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should show a spinner and disable both buttons while loading", async () => {
|
|
83
|
+
const user = userEvent.setup();
|
|
84
|
+
render(
|
|
85
|
+
<SplitButton
|
|
86
|
+
loading
|
|
87
|
+
label="Saving"
|
|
88
|
+
items={[{ id: "export", label: "Export" }]}
|
|
89
|
+
/>,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(
|
|
93
|
+
screen.getByRole("status", { name: /loading/i }),
|
|
94
|
+
).toBeInTheDocument();
|
|
95
|
+
for (const button of screen.getAllByRole("button")) {
|
|
96
|
+
expect(button).toBeDisabled();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await user.click(
|
|
100
|
+
screen.getByRole("button", { name: /more saving actions/i }),
|
|
101
|
+
);
|
|
102
|
+
expect(screen.queryByRole("menu")).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should support controlled open state", () => {
|
|
106
|
+
const onOpenChange = vi.fn();
|
|
107
|
+
render(
|
|
108
|
+
<SplitButton
|
|
109
|
+
open
|
|
110
|
+
onOpenChange={onOpenChange}
|
|
111
|
+
label="Save"
|
|
112
|
+
items={[{ id: "export", label: "Export" }]}
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(screen.getByRole("menuitem", { name: "Export" })).toBeVisible();
|
|
117
|
+
expect(
|
|
118
|
+
screen.getByRole("button", { name: /more save actions/i }),
|
|
119
|
+
).toHaveAttribute("aria-expanded", "true");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should render start and item icons", async () => {
|
|
123
|
+
const user = userEvent.setup();
|
|
124
|
+
render(
|
|
125
|
+
<SplitButton
|
|
126
|
+
label="Save"
|
|
127
|
+
startIcon={<span data-testid="start-icon" />}
|
|
128
|
+
items={[
|
|
129
|
+
{
|
|
130
|
+
id: "export",
|
|
131
|
+
label: "Export",
|
|
132
|
+
icon: <span data-testid="item-icon" />,
|
|
133
|
+
},
|
|
134
|
+
]}
|
|
135
|
+
/>,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(screen.getByTestId("start-icon")).toBeInTheDocument();
|
|
139
|
+
await user.click(
|
|
140
|
+
screen.getByRole("button", { name: /more save actions/i }),
|
|
141
|
+
);
|
|
142
|
+
expect(screen.getByTestId("item-icon")).toBeInTheDocument();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should apply variant aliases and fullWidth layout", () => {
|
|
146
|
+
render(
|
|
147
|
+
<SplitButton
|
|
148
|
+
fullWidth
|
|
149
|
+
label="Save"
|
|
150
|
+
variant="danger"
|
|
151
|
+
items={[{ id: "export", label: "Export" }]}
|
|
152
|
+
/>,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(document.querySelector('[data-slot="split-button"]')).toHaveClass(
|
|
156
|
+
"w-full",
|
|
157
|
+
);
|
|
158
|
+
expect(screen.getByRole("button", { name: "Save" }).className).toMatch(
|
|
159
|
+
/destructive/,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should forward ref to the root element", () => {
|
|
164
|
+
const ref = createRef<HTMLDivElement>();
|
|
165
|
+
render(<SplitButton ref={ref} label="Save" items={items} />);
|
|
166
|
+
|
|
167
|
+
expect(ref.current?.getAttribute("data-slot")).toBe("split-button");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should open the menu on hover and close on mouse leave when triggerOn is hover", async () => {
|
|
171
|
+
const user = userEvent.setup();
|
|
172
|
+
render(
|
|
173
|
+
<SplitButton
|
|
174
|
+
triggerOn="hover"
|
|
175
|
+
label="Save"
|
|
176
|
+
items={[{ id: "export", label: "Export" }]}
|
|
177
|
+
/>,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const trigger = screen.getByRole("button", { name: /more save actions/i });
|
|
181
|
+
await user.hover(trigger);
|
|
182
|
+
expect(
|
|
183
|
+
await screen.findByRole("menuitem", { name: "Export" }),
|
|
184
|
+
).toBeVisible();
|
|
185
|
+
|
|
186
|
+
await user.unhover(trigger);
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
expect(screen.queryByRole("menuitem", { name: "Export" })).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should not open on hover when disabled with triggerOn hover", async () => {
|
|
193
|
+
const user = userEvent.setup();
|
|
194
|
+
render(
|
|
195
|
+
<SplitButton
|
|
196
|
+
triggerOn="hover"
|
|
197
|
+
disabled
|
|
198
|
+
label="Save"
|
|
199
|
+
items={[{ id: "export", label: "Export" }]}
|
|
200
|
+
/>,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
await user.hover(
|
|
204
|
+
screen.getByRole("button", { name: /more save actions/i }),
|
|
205
|
+
);
|
|
206
|
+
expect(screen.queryByRole("menu")).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// split-button.tsx — default static entry (no framer-motion)
|
|
2
|
+
import { SplitButtonBase } from "./split-button-base";
|
|
3
|
+
import type { SplitButtonProps } from "./types";
|
|
4
|
+
|
|
5
|
+
export const SplitButton = (props: SplitButtonProps) => {
|
|
6
|
+
return <SplitButtonBase {...props} />;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
SplitButton.displayName = "SplitButton";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentPropsWithRef,
|
|
3
|
+
MouseEventHandler,
|
|
4
|
+
ReactNode,
|
|
5
|
+
} from "react";
|
|
6
|
+
|
|
7
|
+
import type { ButtonSharedStatic } from "../buttons";
|
|
8
|
+
|
|
9
|
+
export type SplitButtonAppearance = ButtonSharedStatic["appearance"];
|
|
10
|
+
export type SplitButtonSize = Extract<ButtonSharedStatic["size"], string>;
|
|
11
|
+
export type SplitButtonVariant =
|
|
12
|
+
| "primary"
|
|
13
|
+
| "secondary"
|
|
14
|
+
| "outline"
|
|
15
|
+
| "ghost"
|
|
16
|
+
| "danger"
|
|
17
|
+
| "success";
|
|
18
|
+
|
|
19
|
+
export interface SplitButtonItem {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
icon?: ReactNode;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
onSelect?: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SplitButtonProps extends Omit<
|
|
28
|
+
ComponentPropsWithRef<"div">,
|
|
29
|
+
"children" | "onClick"
|
|
30
|
+
> {
|
|
31
|
+
label: string;
|
|
32
|
+
onClick?: MouseEventHandler<HTMLButtonElement>;
|
|
33
|
+
items: SplitButtonItem[];
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
loading?: boolean;
|
|
36
|
+
appearance?: SplitButtonAppearance;
|
|
37
|
+
variant?: SplitButtonVariant;
|
|
38
|
+
size?: SplitButtonSize;
|
|
39
|
+
startIcon?: ReactNode;
|
|
40
|
+
fullWidth?: boolean;
|
|
41
|
+
open?: boolean;
|
|
42
|
+
defaultOpen?: boolean;
|
|
43
|
+
onOpenChange?: (open: boolean) => void;
|
|
44
|
+
triggerLabel?: string;
|
|
45
|
+
triggerOn?: "click" | "hover";
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
zuiSplitButtonContent,
|
|
5
|
+
zuiSplitButtonDropdown,
|
|
6
|
+
zuiSplitButtonFullWidth,
|
|
7
|
+
zuiSplitButtonGroup,
|
|
8
|
+
zuiSplitButtonItemDisabled,
|
|
9
|
+
zuiSplitButtonPrimary,
|
|
10
|
+
zuiSplitButtonRoot,
|
|
11
|
+
zuiSplitButtonTrigger,
|
|
12
|
+
zuiSplitButtonTriggerSizes,
|
|
13
|
+
} from "../../design-system";
|
|
14
|
+
|
|
15
|
+
export const splitButtonRootVariants = cva(zuiSplitButtonRoot, {
|
|
16
|
+
variants: {
|
|
17
|
+
fullWidth: {
|
|
18
|
+
true: zuiSplitButtonFullWidth,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
export const splitButtonDropdownVariants = cva(zuiSplitButtonDropdown, {
|
|
23
|
+
variants: {
|
|
24
|
+
fullWidth: {
|
|
25
|
+
true: zuiSplitButtonFullWidth,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
export const splitButtonGroupVariants = cva(zuiSplitButtonGroup, {
|
|
30
|
+
variants: {
|
|
31
|
+
fullWidth: {
|
|
32
|
+
true: zuiSplitButtonFullWidth,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
export const splitButtonPrimaryVariants = cva(zuiSplitButtonPrimary);
|
|
37
|
+
export const splitButtonTriggerVariants = cva(zuiSplitButtonTrigger, {
|
|
38
|
+
variants: {
|
|
39
|
+
size: zuiSplitButtonTriggerSizes,
|
|
40
|
+
},
|
|
41
|
+
defaultVariants: {
|
|
42
|
+
size: "md",
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
export const splitButtonContentVariants = cva(zuiSplitButtonContent);
|
|
46
|
+
export const splitButtonItemDisabledVariants = cva(zuiSplitButtonItemDisabled);
|
package/dist/chunk-5FU57ZVQ.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var chunkTJ2EWPER_js = require('./chunk-TJ2EWPER.js');
|
|
4
|
-
var classVarianceAuthority = require('class-variance-authority');
|
|
5
|
-
|
|
6
|
-
var buttonVariants = classVarianceAuthority.cva(chunkTJ2EWPER_js.zuiButtonBase, {
|
|
7
|
-
variants: {
|
|
8
|
-
appearance: chunkTJ2EWPER_js.zuiButtonAppearances,
|
|
9
|
-
size: chunkTJ2EWPER_js.zuiButtonSizes
|
|
10
|
-
},
|
|
11
|
-
defaultVariants: {
|
|
12
|
-
appearance: "default",
|
|
13
|
-
size: "md"
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
exports.buttonVariants = buttonVariants;
|
|
18
|
-
//# sourceMappingURL=chunk-5FU57ZVQ.js.map
|
|
19
|
-
//# sourceMappingURL=chunk-5FU57ZVQ.js.map
|