@rovula/ui 0.1.34 → 0.1.36
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/dist/cjs/bundle.css +3 -0
- package/dist/cjs/bundle.js +1 -1
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/DropdownMenu/DropdownMenu.d.ts +8 -1
- package/dist/cjs/types/patterns/form-dialog/FormDialog.d.ts +5 -0
- package/dist/cjs/types/patterns/form-dialog/FormDialog.stories.d.ts +5 -0
- package/dist/cjs/types/patterns/menu/Menu.d.ts +15 -1
- package/dist/cjs/types/patterns/menu/Menu.stories.d.ts +3 -1
- package/dist/components/DropdownMenu/DropdownMenu.js +2 -2
- package/dist/esm/bundle.css +3 -0
- package/dist/esm/bundle.js +1 -1
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/DropdownMenu/DropdownMenu.d.ts +8 -1
- package/dist/esm/types/patterns/form-dialog/FormDialog.d.ts +5 -0
- package/dist/esm/types/patterns/form-dialog/FormDialog.stories.d.ts +5 -0
- package/dist/esm/types/patterns/menu/Menu.d.ts +15 -1
- package/dist/esm/types/patterns/menu/Menu.stories.d.ts +3 -1
- package/dist/index.d.ts +28 -2
- package/dist/patterns/form-dialog/FormDialog.js +3 -2
- package/dist/patterns/menu/Menu.js +5 -5
- package/dist/patterns/menu/Menu.stories.js +20 -0
- package/dist/src/theme/global.css +4 -0
- package/package.json +1 -1
- package/src/components/DropdownMenu/DropdownMenu.tsx +10 -3
- package/src/patterns/form-dialog/FormDialog.tsx +16 -5
- package/src/patterns/menu/Menu.stories.tsx +63 -0
- package/src/patterns/menu/Menu.tsx +19 -1
|
@@ -10,7 +10,14 @@ declare const DropdownMenuSubTrigger: React.ForwardRefExoticComponent<Omit<Dropd
|
|
|
10
10
|
inset?: boolean;
|
|
11
11
|
} & React.RefAttributes<HTMLDivElement>>;
|
|
12
12
|
declare const DropdownMenuSubContent: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuSubContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
13
|
-
declare const DropdownMenuContent: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuContentProps & React.RefAttributes<HTMLDivElement>, "ref"> &
|
|
13
|
+
declare const DropdownMenuContent: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
|
|
14
|
+
/**
|
|
15
|
+
* DOM element to render the portal into.
|
|
16
|
+
* Pass the dialog's content element when using inside a Dialog to avoid
|
|
17
|
+
* Radix pointer-events conflicts that break hover after re-open.
|
|
18
|
+
*/
|
|
19
|
+
container?: HTMLElement | null;
|
|
20
|
+
} & React.RefAttributes<HTMLDivElement>>;
|
|
14
21
|
declare const DropdownMenuItem: React.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & {
|
|
15
22
|
inset?: boolean;
|
|
16
23
|
selected?: boolean;
|
|
@@ -31,6 +31,11 @@ export type FormDialogProps = {
|
|
|
31
31
|
extraAction?: FormDialogAction;
|
|
32
32
|
scrollable?: boolean;
|
|
33
33
|
className?: string;
|
|
34
|
+
headerClassName?: string;
|
|
35
|
+
titleClassName?: string;
|
|
36
|
+
descriptionClassName?: string;
|
|
37
|
+
bodyClassName?: string;
|
|
38
|
+
footerClassName?: string;
|
|
34
39
|
/**
|
|
35
40
|
* When provided, the confirm button becomes type="submit" and is linked to this form id.
|
|
36
41
|
* Use together with a <Form id={formId} .../> inside children.
|
|
@@ -20,6 +20,11 @@ declare const meta: {
|
|
|
20
20
|
extraAction?: import("./FormDialog").FormDialogAction | undefined;
|
|
21
21
|
scrollable?: boolean | undefined;
|
|
22
22
|
className?: string | undefined;
|
|
23
|
+
headerClassName?: string | undefined;
|
|
24
|
+
titleClassName?: string | undefined;
|
|
25
|
+
descriptionClassName?: string | undefined;
|
|
26
|
+
bodyClassName?: string | undefined;
|
|
27
|
+
footerClassName?: string | undefined;
|
|
23
28
|
formId?: string | undefined;
|
|
24
29
|
testId?: string | undefined;
|
|
25
30
|
}>) => import("react/jsx-runtime").JSX.Element)[];
|
|
@@ -16,6 +16,8 @@ export type MenuOption = {
|
|
|
16
16
|
onClick?: () => void;
|
|
17
17
|
/** data-testid attached to the item element */
|
|
18
18
|
testId?: string;
|
|
19
|
+
/** Custom className applied to the item element */
|
|
20
|
+
className?: string;
|
|
19
21
|
};
|
|
20
22
|
export type MenuItemType = {
|
|
21
23
|
type: "item";
|
|
@@ -65,9 +67,21 @@ export type MenuProps = {
|
|
|
65
67
|
contentClassName?: string;
|
|
66
68
|
/** data-testid attached to the menu content element */
|
|
67
69
|
testId?: string;
|
|
70
|
+
/**
|
|
71
|
+
* DOM element to render the dropdown portal into.
|
|
72
|
+
* Pass the dialog's content element when using inside a Dialog to avoid
|
|
73
|
+
* Radix pointer-events conflicts that break hover after re-open.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* const [container, setContainer] = React.useState<HTMLDivElement | null>(null);
|
|
77
|
+
* <DialogContent ref={setContainer}>
|
|
78
|
+
* <Menu container={container} ... />
|
|
79
|
+
* </DialogContent>
|
|
80
|
+
*/
|
|
81
|
+
container?: HTMLElement | null;
|
|
68
82
|
};
|
|
69
83
|
export declare const Menu: {
|
|
70
|
-
({ trigger, items, selectedValues, onSelect, header, open, onOpenChange, align, side, sideOffset, contentClassName, testId, }: MenuProps): import("react/jsx-runtime").JSX.Element;
|
|
84
|
+
({ trigger, items, selectedValues, onSelect, header, open, onOpenChange, align, side, sideOffset, contentClassName, testId, container, }: MenuProps): import("react/jsx-runtime").JSX.Element;
|
|
71
85
|
displayName: string;
|
|
72
86
|
};
|
|
73
87
|
export { DropdownMenuItem as MenuItem, DropdownMenuSeparator as MenuSeparator, DropdownMenuLabel as MenuLabel, } from "../../components/DropdownMenu/DropdownMenu";
|
|
@@ -4,7 +4,7 @@ import { Menu, MenuItemType } from "./Menu";
|
|
|
4
4
|
declare const meta: {
|
|
5
5
|
title: string;
|
|
6
6
|
component: {
|
|
7
|
-
({ trigger, items, selectedValues, onSelect, header, open, onOpenChange, align, side, sideOffset, contentClassName, testId, }: import("./Menu").MenuProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
({ trigger, items, selectedValues, onSelect, header, open, onOpenChange, align, side, sideOffset, contentClassName, testId, container, }: import("./Menu").MenuProps): import("react/jsx-runtime").JSX.Element;
|
|
8
8
|
displayName: string;
|
|
9
9
|
};
|
|
10
10
|
parameters: {
|
|
@@ -23,6 +23,7 @@ declare const meta: {
|
|
|
23
23
|
sideOffset?: number | undefined;
|
|
24
24
|
contentClassName?: string | undefined;
|
|
25
25
|
testId?: string | undefined;
|
|
26
|
+
container?: (HTMLElement | null) | undefined;
|
|
26
27
|
}>) => import("react/jsx-runtime").JSX.Element)[];
|
|
27
28
|
};
|
|
28
29
|
export default meta;
|
|
@@ -37,3 +38,4 @@ export declare const ComplexMenu: StoryObj<typeof Menu>;
|
|
|
37
38
|
export declare const MultiSelectPattern: StoryObj<typeof Menu>;
|
|
38
39
|
export declare const ChangeStatus: StoryObj<typeof Menu>;
|
|
39
40
|
export declare const ManageColumn: StoryObj<typeof Menu>;
|
|
41
|
+
export declare const InsideDialog: StoryObj<typeof Menu>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1281,7 +1281,14 @@ declare const DropdownMenuSubTrigger: React$1.ForwardRefExoticComponent<Omit<Dro
|
|
|
1281
1281
|
inset?: boolean;
|
|
1282
1282
|
} & React$1.RefAttributes<HTMLDivElement>>;
|
|
1283
1283
|
declare const DropdownMenuSubContent: React$1.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuSubContentProps & React$1.RefAttributes<HTMLDivElement>, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
|
|
1284
|
-
declare const DropdownMenuContent: React$1.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuContentProps & React$1.RefAttributes<HTMLDivElement>, "ref"> &
|
|
1284
|
+
declare const DropdownMenuContent: React$1.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuContentProps & React$1.RefAttributes<HTMLDivElement>, "ref"> & {
|
|
1285
|
+
/**
|
|
1286
|
+
* DOM element to render the portal into.
|
|
1287
|
+
* Pass the dialog's content element when using inside a Dialog to avoid
|
|
1288
|
+
* Radix pointer-events conflicts that break hover after re-open.
|
|
1289
|
+
*/
|
|
1290
|
+
container?: HTMLElement | null;
|
|
1291
|
+
} & React$1.RefAttributes<HTMLDivElement>>;
|
|
1285
1292
|
declare const DropdownMenuItem: React$1.ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuItemProps & React$1.RefAttributes<HTMLDivElement>, "ref"> & {
|
|
1286
1293
|
inset?: boolean;
|
|
1287
1294
|
selected?: boolean;
|
|
@@ -1728,6 +1735,11 @@ type FormDialogProps = {
|
|
|
1728
1735
|
extraAction?: FormDialogAction;
|
|
1729
1736
|
scrollable?: boolean;
|
|
1730
1737
|
className?: string;
|
|
1738
|
+
headerClassName?: string;
|
|
1739
|
+
titleClassName?: string;
|
|
1740
|
+
descriptionClassName?: string;
|
|
1741
|
+
bodyClassName?: string;
|
|
1742
|
+
footerClassName?: string;
|
|
1731
1743
|
/**
|
|
1732
1744
|
* When provided, the confirm button becomes type="submit" and is linked to this form id.
|
|
1733
1745
|
* Use together with a <Form id={formId} .../> inside children.
|
|
@@ -1754,6 +1766,8 @@ type MenuOption = {
|
|
|
1754
1766
|
onClick?: () => void;
|
|
1755
1767
|
/** data-testid attached to the item element */
|
|
1756
1768
|
testId?: string;
|
|
1769
|
+
/** Custom className applied to the item element */
|
|
1770
|
+
className?: string;
|
|
1757
1771
|
};
|
|
1758
1772
|
type MenuItemType = {
|
|
1759
1773
|
type: "item";
|
|
@@ -1803,9 +1817,21 @@ type MenuProps = {
|
|
|
1803
1817
|
contentClassName?: string;
|
|
1804
1818
|
/** data-testid attached to the menu content element */
|
|
1805
1819
|
testId?: string;
|
|
1820
|
+
/**
|
|
1821
|
+
* DOM element to render the dropdown portal into.
|
|
1822
|
+
* Pass the dialog's content element when using inside a Dialog to avoid
|
|
1823
|
+
* Radix pointer-events conflicts that break hover after re-open.
|
|
1824
|
+
*
|
|
1825
|
+
* @example
|
|
1826
|
+
* const [container, setContainer] = React.useState<HTMLDivElement | null>(null);
|
|
1827
|
+
* <DialogContent ref={setContainer}>
|
|
1828
|
+
* <Menu container={container} ... />
|
|
1829
|
+
* </DialogContent>
|
|
1830
|
+
*/
|
|
1831
|
+
container?: HTMLElement | null;
|
|
1806
1832
|
};
|
|
1807
1833
|
declare const Menu: {
|
|
1808
|
-
({ trigger, items, selectedValues, onSelect, header, open, onOpenChange, align, side, sideOffset, contentClassName, testId, }: MenuProps): react_jsx_runtime.JSX.Element;
|
|
1834
|
+
({ trigger, items, selectedValues, onSelect, header, open, onOpenChange, align, side, sideOffset, contentClassName, testId, container, }: MenuProps): react_jsx_runtime.JSX.Element;
|
|
1809
1835
|
displayName: string;
|
|
1810
1836
|
};
|
|
1811
1837
|
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogTrigger, } from "@/components/Dialog/Dialog";
|
|
4
4
|
import Button from "@/components/Button/Button";
|
|
5
|
-
|
|
5
|
+
import { cn } from "@/utils/cn";
|
|
6
|
+
export const FormDialog = ({ open, onOpenChange, title, description, children, trigger, confirmAction, cancelAction, extraAction, scrollable = false, className, headerClassName, titleClassName, descriptionClassName, bodyClassName, footerClassName, formId, testId, }) => {
|
|
6
7
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
7
8
|
const hasFooter = confirmAction || cancelAction || extraAction;
|
|
8
|
-
return (_jsxs(Dialog, { open: open, onOpenChange: onOpenChange, children: [trigger && _jsx(DialogTrigger, { asChild: true, children: trigger }), _jsxs(DialogContent, { className: className, "data-testid": testId, children: [_jsxs(DialogHeader, { children: [title && (_jsx(DialogTitle, { "data-testid": testId && `${testId}-title`, children: title })), description && (_jsx(DialogDescription, { "data-testid": testId && `${testId}-description`, children: description }))] }), children && (_jsx(DialogBody, { scrollable: scrollable, children: children })), hasFooter && (_jsxs(DialogFooter, { className: extraAction
|
|
9
|
+
return (_jsxs(Dialog, { open: open, onOpenChange: onOpenChange, children: [trigger && _jsx(DialogTrigger, { asChild: true, children: trigger }), _jsxs(DialogContent, { className: className, "data-testid": testId, children: [_jsxs(DialogHeader, { className: headerClassName, children: [title && (_jsx(DialogTitle, { className: titleClassName, "data-testid": testId && `${testId}-title`, children: title })), description && (_jsx(DialogDescription, { className: descriptionClassName, "data-testid": testId && `${testId}-description`, children: description }))] }), children && (_jsx(DialogBody, { className: bodyClassName, scrollable: scrollable, children: children })), hasFooter && (_jsxs(DialogFooter, { className: cn(extraAction && "justify-between", footerClassName), children: [extraAction && (_jsx(Button, { type: (_a = extraAction.type) !== null && _a !== void 0 ? _a : "button", variant: (_b = extraAction.variant) !== null && _b !== void 0 ? _b : "outline", color: (_c = extraAction.color) !== null && _c !== void 0 ? _c : "secondary", fullwidth: false, disabled: extraAction.disabled, isLoading: extraAction.isLoading, onClick: extraAction.onClick, className: extraAction.className, "data-testid": testId && `${testId}-extra-button`, children: extraAction.label })), _jsxs("div", { className: "flex items-center gap-4", children: [cancelAction && (_jsx(Button, { type: (_d = cancelAction.type) !== null && _d !== void 0 ? _d : "button", variant: (_e = cancelAction.variant) !== null && _e !== void 0 ? _e : "outline", color: (_f = cancelAction.color) !== null && _f !== void 0 ? _f : "primary", fullwidth: false, disabled: cancelAction.disabled, isLoading: cancelAction.isLoading, onClick: cancelAction.onClick, className: cancelAction.className, "data-testid": testId && `${testId}-cancel-button`, children: cancelAction.label })), confirmAction && (_jsx(Button, { type: formId ? "submit" : (_g = confirmAction.type) !== null && _g !== void 0 ? _g : "button", form: formId, variant: (_h = confirmAction.variant) !== null && _h !== void 0 ? _h : "solid", color: (_j = confirmAction.color) !== null && _j !== void 0 ? _j : "primary", fullwidth: false, disabled: confirmAction.disabled, isLoading: confirmAction.isLoading, onClick: confirmAction.onClick, className: confirmAction.className, "data-testid": testId && `${testId}-confirm-button`, children: confirmAction.label }))] })] }))] })] }));
|
|
9
10
|
};
|
|
10
11
|
FormDialog.displayName = "FormDialog";
|
|
@@ -23,7 +23,7 @@ function renderMenuItems(items, selectedValues, onSelect) {
|
|
|
23
23
|
onSelect === null || onSelect === void 0 ? void 0 : onSelect(val, opt);
|
|
24
24
|
(_a = opt.onClick) === null || _a === void 0 ? void 0 : _a.call(opt);
|
|
25
25
|
}
|
|
26
|
-
}, children: buffer.map((opt) => (_jsx(DropdownMenuRadioItem, { value: opt.value, disabled: opt.disabled, icon: opt.icon, "data-testid": opt.testId, children: opt.label }, opt.value))) }, key));
|
|
26
|
+
}, children: buffer.map((opt) => (_jsx(DropdownMenuRadioItem, { value: opt.value, disabled: opt.disabled, icon: opt.icon, "data-testid": opt.testId, className: opt.className, children: opt.label }, opt.value))) }, key));
|
|
27
27
|
};
|
|
28
28
|
items.forEach((item, index) => {
|
|
29
29
|
var _a;
|
|
@@ -54,7 +54,7 @@ function renderMenuItems(items, selectedValues, onSelect) {
|
|
|
54
54
|
const opt = item.item;
|
|
55
55
|
const isSelected = (_a = opt.checked) !== null && _a !== void 0 ? _a : selectedValues.includes(opt.value);
|
|
56
56
|
if (opt.type === "checkbox") {
|
|
57
|
-
result.push(_jsx(DropdownMenuCheckboxItem, { checked: isSelected, disabled: opt.disabled, "data-testid": opt.testId, onCheckedChange: () => {
|
|
57
|
+
result.push(_jsx(DropdownMenuCheckboxItem, { checked: isSelected, disabled: opt.disabled, "data-testid": opt.testId, className: opt.className, onCheckedChange: () => {
|
|
58
58
|
var _a;
|
|
59
59
|
onSelect === null || onSelect === void 0 ? void 0 : onSelect(opt.value, opt);
|
|
60
60
|
(_a = opt.onClick) === null || _a === void 0 ? void 0 : _a.call(opt);
|
|
@@ -62,7 +62,7 @@ function renderMenuItems(items, selectedValues, onSelect) {
|
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
64
|
// default item
|
|
65
|
-
result.push(_jsx(DropdownMenuItem, { selected: isSelected, icon: opt.icon, disabled: opt.disabled, className: cn(opt.danger && "text-red-500"), "data-testid": opt.testId, onSelect: () => {
|
|
65
|
+
result.push(_jsx(DropdownMenuItem, { selected: isSelected, icon: opt.icon, disabled: opt.disabled, className: cn(opt.danger && "text-red-500", opt.className), "data-testid": opt.testId, onSelect: () => {
|
|
66
66
|
var _a;
|
|
67
67
|
onSelect === null || onSelect === void 0 ? void 0 : onSelect(opt.value, opt);
|
|
68
68
|
(_a = opt.onClick) === null || _a === void 0 ? void 0 : _a.call(opt);
|
|
@@ -86,12 +86,12 @@ function renderMenuItems(items, selectedValues, onSelect) {
|
|
|
86
86
|
// - Full a11y / WAI-ARIA
|
|
87
87
|
// - Sub-menu support via DropdownMenuSub
|
|
88
88
|
// ---------------------------------------------------------------------------
|
|
89
|
-
export const Menu = ({ trigger, items, selectedValues = [], onSelect, header, open, onOpenChange, align = "start", side = "bottom", sideOffset = 4, contentClassName, testId, }) => (
|
|
89
|
+
export const Menu = ({ trigger, items, selectedValues = [], onSelect, header, open, onOpenChange, align = "start", side = "bottom", sideOffset = 4, contentClassName, testId, container, }) => (
|
|
90
90
|
// Stop click events from bubbling through React's portal event system.
|
|
91
91
|
// DropdownMenuContent renders in a DOM portal (document.body) but React
|
|
92
92
|
// synthetic events still bubble through the component tree, so clicks on
|
|
93
93
|
// menu items reach ancestor onClick handlers (e.g. a clickable card).
|
|
94
|
-
_jsx("div", { onClick: (e) => e.stopPropagation(), children: _jsxs(DropdownMenuRoot, { open: open, onOpenChange: onOpenChange, children: [trigger && (_jsx(DropdownMenuTrigger, { asChild: true, children: trigger })), _jsxs(DropdownMenuContent, { align: align, side: side, sideOffset: sideOffset, className: contentClassName, "data-testid": testId, children: [header && (_jsx("div", { className: "sticky top-0 z-10 bg-modal-surface border-b border-[var(--dropdown-menu-seperator-bg)]", children: header })), renderMenuItems(items, selectedValues, onSelect)] })] }) }));
|
|
94
|
+
_jsx("div", { onClick: (e) => e.stopPropagation(), children: _jsxs(DropdownMenuRoot, { open: open, onOpenChange: onOpenChange, children: [trigger && (_jsx(DropdownMenuTrigger, { asChild: true, children: trigger })), _jsxs(DropdownMenuContent, { align: align, side: side, sideOffset: sideOffset, className: contentClassName, "data-testid": testId, container: container, children: [header && (_jsx("div", { className: "sticky top-0 z-10 bg-modal-surface border-b border-[var(--dropdown-menu-seperator-bg)]", children: header })), renderMenuItems(items, selectedValues, onSelect)] })] }) }));
|
|
95
95
|
Menu.displayName = "Menu";
|
|
96
96
|
// ---------------------------------------------------------------------------
|
|
97
97
|
// Re-exports — backward compat for consumers using Menu's sub-components
|
|
@@ -6,6 +6,7 @@ import ActionButton from "../../components/ActionButton/ActionButton";
|
|
|
6
6
|
import Icon from "../../components/Icon/Icon";
|
|
7
7
|
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
|
8
8
|
import { Switch } from "../../components/Switch/Switch";
|
|
9
|
+
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogBody, DialogFooter, } from "../../components/Dialog/Dialog";
|
|
9
10
|
const meta = {
|
|
10
11
|
title: "Patterns/Menu",
|
|
11
12
|
component: Menu,
|
|
@@ -609,3 +610,22 @@ export const ManageColumn = {
|
|
|
609
610
|
return (_jsxs("div", { className: "flex gap-8 items-start", children: [_jsxs("div", { children: [_jsx("p", { className: "typography-small4 text-text-g-contrast-medium mb-2", children: "Manage Column panel" }), _jsx(Menu, { trigger: _jsxs(Button, { variant: "outline", children: [_jsx(Icon, { type: "heroicons", name: "view-columns", className: "size-4 mr-2" }), "Manage Columns"] }), open: open, onOpenChange: setOpen, header: _jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "typography-subtitle4 text-text-g-contrast-high", children: "Manage column" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { className: "typography-small4 text-text-g-contrast-medium hover:text-text-g-contrast-high transition-colors", onClick: hideAll, children: "Hide all" }), _jsx("button", { className: "typography-small4 text-[var(--dropdown-menu-checkbox-checked-bg)] hover:opacity-80 transition-opacity", onClick: showAll, children: "Show all" }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => setOpen(false), children: "Done" })] })] }), items: items, contentClassName: "w-80" })] }), _jsxs("div", { className: "text-sm text-text-g-contrast-medium", children: [_jsx("p", { className: "font-semibold mb-1", children: "Columns:" }), _jsx("pre", { className: "text-xs", children: JSON.stringify(columns.map((c) => ({ [c.label]: c.visible })), null, 2) })] })] }));
|
|
610
611
|
},
|
|
611
612
|
};
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
// Inside Dialog — validates that hover works after dialog re-open
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
const dialogItems = [
|
|
617
|
+
{ type: "item", item: { value: "edit", label: "Edit" } },
|
|
618
|
+
{ type: "item", item: { value: "duplicate", label: "Duplicate" } },
|
|
619
|
+
{ type: "separator" },
|
|
620
|
+
{
|
|
621
|
+
type: "item",
|
|
622
|
+
item: { value: "delete", label: "Delete", danger: true },
|
|
623
|
+
},
|
|
624
|
+
];
|
|
625
|
+
export const InsideDialog = {
|
|
626
|
+
name: "Inside Dialog (hover fix)",
|
|
627
|
+
render: () => {
|
|
628
|
+
const [container, setContainer] = useState(null);
|
|
629
|
+
return (_jsxs(Dialog, { children: [_jsx(DialogTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", children: "Open Dialog" }) }), _jsxs(DialogContent, { ref: setContainer, children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Menu inside Dialog" }) }), _jsxs(DialogBody, { children: [_jsx("p", { className: "typography-body3 text-text-g-contrast-medium mb-6", children: "Close and re-open the dialog \u2014 hover on menu items should still work correctly every time." }), _jsx(Menu, { trigger: _jsx(ActionButton, { variant: "icon", children: _jsx(Icon, { type: "heroicons", name: "ellipsis-vertical" }) }), items: dialogItems, container: container, onSelect: (v) => console.log("selected:", v) })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", children: "Cancel" }), _jsx(Button, { children: "Confirm" })] })] })] }));
|
|
630
|
+
},
|
|
631
|
+
};
|
package/package.json
CHANGED
|
@@ -66,9 +66,16 @@ DropdownMenuSubContent.displayName =
|
|
|
66
66
|
|
|
67
67
|
const DropdownMenuContent = React.forwardRef<
|
|
68
68
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
69
|
-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
|
|
70
|
+
/**
|
|
71
|
+
* DOM element to render the portal into.
|
|
72
|
+
* Pass the dialog's content element when using inside a Dialog to avoid
|
|
73
|
+
* Radix pointer-events conflicts that break hover after re-open.
|
|
74
|
+
*/
|
|
75
|
+
container?: HTMLElement | null;
|
|
76
|
+
}
|
|
77
|
+
>(({ className, sideOffset = 4, container, ...props }, ref) => (
|
|
78
|
+
<DropdownMenuPrimitive.Portal container={container}>
|
|
72
79
|
<DropdownMenuPrimitive.Content
|
|
73
80
|
ref={ref}
|
|
74
81
|
sideOffset={sideOffset}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@/components/Dialog/Dialog";
|
|
14
14
|
import Button from "@/components/Button/Button";
|
|
15
15
|
import Loading from "@/components/Loading/Loading";
|
|
16
|
+
import { cn } from "@/utils/cn";
|
|
16
17
|
|
|
17
18
|
export type FormDialogAction = {
|
|
18
19
|
label: string;
|
|
@@ -46,6 +47,11 @@ export type FormDialogProps = {
|
|
|
46
47
|
extraAction?: FormDialogAction;
|
|
47
48
|
scrollable?: boolean;
|
|
48
49
|
className?: string;
|
|
50
|
+
headerClassName?: string;
|
|
51
|
+
titleClassName?: string;
|
|
52
|
+
descriptionClassName?: string;
|
|
53
|
+
bodyClassName?: string;
|
|
54
|
+
footerClassName?: string;
|
|
49
55
|
/**
|
|
50
56
|
* When provided, the confirm button becomes type="submit" and is linked to this form id.
|
|
51
57
|
* Use together with a <Form id={formId} .../> inside children.
|
|
@@ -66,6 +72,11 @@ export const FormDialog: React.FC<FormDialogProps> = ({
|
|
|
66
72
|
extraAction,
|
|
67
73
|
scrollable = false,
|
|
68
74
|
className,
|
|
75
|
+
headerClassName,
|
|
76
|
+
titleClassName,
|
|
77
|
+
descriptionClassName,
|
|
78
|
+
bodyClassName,
|
|
79
|
+
footerClassName,
|
|
69
80
|
formId,
|
|
70
81
|
testId,
|
|
71
82
|
}) => {
|
|
@@ -75,25 +86,25 @@ export const FormDialog: React.FC<FormDialogProps> = ({
|
|
|
75
86
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
76
87
|
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
|
|
77
88
|
<DialogContent className={className} data-testid={testId}>
|
|
78
|
-
<DialogHeader>
|
|
89
|
+
<DialogHeader className={headerClassName}>
|
|
79
90
|
{title && (
|
|
80
|
-
<DialogTitle data-testid={testId && `${testId}-title`}>
|
|
91
|
+
<DialogTitle className={titleClassName} data-testid={testId && `${testId}-title`}>
|
|
81
92
|
{title}
|
|
82
93
|
</DialogTitle>
|
|
83
94
|
)}
|
|
84
95
|
{description && (
|
|
85
|
-
<DialogDescription data-testid={testId && `${testId}-description`}>
|
|
96
|
+
<DialogDescription className={descriptionClassName} data-testid={testId && `${testId}-description`}>
|
|
86
97
|
{description}
|
|
87
98
|
</DialogDescription>
|
|
88
99
|
)}
|
|
89
100
|
</DialogHeader>
|
|
90
101
|
|
|
91
102
|
{children && (
|
|
92
|
-
<DialogBody scrollable={scrollable}>{children}</DialogBody>
|
|
103
|
+
<DialogBody className={bodyClassName} scrollable={scrollable}>{children}</DialogBody>
|
|
93
104
|
)}
|
|
94
105
|
|
|
95
106
|
{hasFooter && (
|
|
96
|
-
<DialogFooter className={extraAction
|
|
107
|
+
<DialogFooter className={cn(extraAction && "justify-between", footerClassName)}>
|
|
97
108
|
{extraAction && (
|
|
98
109
|
<Button
|
|
99
110
|
type={extraAction.type ?? "button"}
|
|
@@ -6,6 +6,15 @@ import ActionButton from "../../components/ActionButton/ActionButton";
|
|
|
6
6
|
import Icon from "../../components/Icon/Icon";
|
|
7
7
|
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
|
8
8
|
import { Switch } from "../../components/Switch/Switch";
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogTrigger,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
DialogBody,
|
|
16
|
+
DialogFooter,
|
|
17
|
+
} from "../../components/Dialog/Dialog";
|
|
9
18
|
|
|
10
19
|
const meta = {
|
|
11
20
|
title: "Patterns/Menu",
|
|
@@ -1098,3 +1107,57 @@ export const ManageColumn: StoryObj<typeof Menu> = {
|
|
|
1098
1107
|
);
|
|
1099
1108
|
},
|
|
1100
1109
|
};
|
|
1110
|
+
|
|
1111
|
+
// ---------------------------------------------------------------------------
|
|
1112
|
+
// Inside Dialog — validates that hover works after dialog re-open
|
|
1113
|
+
// ---------------------------------------------------------------------------
|
|
1114
|
+
|
|
1115
|
+
const dialogItems: MenuItemType[] = [
|
|
1116
|
+
{ type: "item", item: { value: "edit", label: "Edit" } },
|
|
1117
|
+
{ type: "item", item: { value: "duplicate", label: "Duplicate" } },
|
|
1118
|
+
{ type: "separator" },
|
|
1119
|
+
{
|
|
1120
|
+
type: "item",
|
|
1121
|
+
item: { value: "delete", label: "Delete", danger: true },
|
|
1122
|
+
},
|
|
1123
|
+
];
|
|
1124
|
+
|
|
1125
|
+
export const InsideDialog: StoryObj<typeof Menu> = {
|
|
1126
|
+
name: "Inside Dialog (hover fix)",
|
|
1127
|
+
render: () => {
|
|
1128
|
+
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
|
1129
|
+
|
|
1130
|
+
return (
|
|
1131
|
+
<Dialog>
|
|
1132
|
+
<DialogTrigger asChild>
|
|
1133
|
+
<Button variant="outline">Open Dialog</Button>
|
|
1134
|
+
</DialogTrigger>
|
|
1135
|
+
<DialogContent ref={setContainer}>
|
|
1136
|
+
<DialogHeader>
|
|
1137
|
+
<DialogTitle>Menu inside Dialog</DialogTitle>
|
|
1138
|
+
</DialogHeader>
|
|
1139
|
+
<DialogBody>
|
|
1140
|
+
<p className="typography-body3 text-text-g-contrast-medium mb-6">
|
|
1141
|
+
Close and re-open the dialog — hover on menu items should still
|
|
1142
|
+
work correctly every time.
|
|
1143
|
+
</p>
|
|
1144
|
+
<Menu
|
|
1145
|
+
trigger={
|
|
1146
|
+
<ActionButton variant="icon">
|
|
1147
|
+
<Icon type="heroicons" name="ellipsis-vertical" />
|
|
1148
|
+
</ActionButton>
|
|
1149
|
+
}
|
|
1150
|
+
items={dialogItems}
|
|
1151
|
+
container={container}
|
|
1152
|
+
onSelect={(v) => console.log("selected:", v)}
|
|
1153
|
+
/>
|
|
1154
|
+
</DialogBody>
|
|
1155
|
+
<DialogFooter>
|
|
1156
|
+
<Button variant="outline">Cancel</Button>
|
|
1157
|
+
<Button>Confirm</Button>
|
|
1158
|
+
</DialogFooter>
|
|
1159
|
+
</DialogContent>
|
|
1160
|
+
</Dialog>
|
|
1161
|
+
);
|
|
1162
|
+
},
|
|
1163
|
+
};
|
|
@@ -38,6 +38,8 @@ export type MenuOption = {
|
|
|
38
38
|
onClick?: () => void;
|
|
39
39
|
/** data-testid attached to the item element */
|
|
40
40
|
testId?: string;
|
|
41
|
+
/** Custom className applied to the item element */
|
|
42
|
+
className?: string;
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
export type MenuItemType =
|
|
@@ -83,6 +85,18 @@ export type MenuProps = {
|
|
|
83
85
|
contentClassName?: string;
|
|
84
86
|
/** data-testid attached to the menu content element */
|
|
85
87
|
testId?: string;
|
|
88
|
+
/**
|
|
89
|
+
* DOM element to render the dropdown portal into.
|
|
90
|
+
* Pass the dialog's content element when using inside a Dialog to avoid
|
|
91
|
+
* Radix pointer-events conflicts that break hover after re-open.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* const [container, setContainer] = React.useState<HTMLDivElement | null>(null);
|
|
95
|
+
* <DialogContent ref={setContainer}>
|
|
96
|
+
* <Menu container={container} ... />
|
|
97
|
+
* </DialogContent>
|
|
98
|
+
*/
|
|
99
|
+
container?: HTMLElement | null;
|
|
86
100
|
};
|
|
87
101
|
|
|
88
102
|
// ---------------------------------------------------------------------------
|
|
@@ -124,6 +138,7 @@ function renderMenuItems(
|
|
|
124
138
|
disabled={opt.disabled}
|
|
125
139
|
icon={opt.icon as React.ReactNode}
|
|
126
140
|
data-testid={opt.testId}
|
|
141
|
+
className={opt.className}
|
|
127
142
|
>
|
|
128
143
|
{opt.label}
|
|
129
144
|
</DropdownMenuRadioItem>
|
|
@@ -191,6 +206,7 @@ function renderMenuItems(
|
|
|
191
206
|
checked={isSelected}
|
|
192
207
|
disabled={opt.disabled}
|
|
193
208
|
data-testid={opt.testId}
|
|
209
|
+
className={opt.className}
|
|
194
210
|
onCheckedChange={() => {
|
|
195
211
|
onSelect?.(opt.value, opt);
|
|
196
212
|
opt.onClick?.();
|
|
@@ -209,7 +225,7 @@ function renderMenuItems(
|
|
|
209
225
|
selected={isSelected}
|
|
210
226
|
icon={opt.icon as React.ReactNode}
|
|
211
227
|
disabled={opt.disabled}
|
|
212
|
-
className={cn(opt.danger && "text-red-500")}
|
|
228
|
+
className={cn(opt.danger && "text-red-500", opt.className)}
|
|
213
229
|
data-testid={opt.testId}
|
|
214
230
|
onSelect={() => {
|
|
215
231
|
onSelect?.(opt.value, opt);
|
|
@@ -254,6 +270,7 @@ export const Menu = ({
|
|
|
254
270
|
sideOffset = 4,
|
|
255
271
|
contentClassName,
|
|
256
272
|
testId,
|
|
273
|
+
container,
|
|
257
274
|
}: MenuProps) => (
|
|
258
275
|
// Stop click events from bubbling through React's portal event system.
|
|
259
276
|
// DropdownMenuContent renders in a DOM portal (document.body) but React
|
|
@@ -270,6 +287,7 @@ export const Menu = ({
|
|
|
270
287
|
sideOffset={sideOffset}
|
|
271
288
|
className={contentClassName}
|
|
272
289
|
data-testid={testId}
|
|
290
|
+
container={container}
|
|
273
291
|
>
|
|
274
292
|
{header && (
|
|
275
293
|
<div className="sticky top-0 z-10 bg-modal-surface border-b border-[var(--dropdown-menu-seperator-bg)]">
|