@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.
@@ -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"> & React.RefAttributes<HTMLDivElement>>;
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"> & React$1.RefAttributes<HTMLDivElement>>;
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
- export const FormDialog = ({ open, onOpenChange, title, description, children, trigger, confirmAction, cancelAction, extraAction, scrollable = false, className, formId, testId, }) => {
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 ? "justify-between" : undefined, 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
+ 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
+ };
@@ -4098,6 +4098,10 @@ input[type=number] {
4098
4098
  margin-bottom: 1rem;
4099
4099
  }
4100
4100
 
4101
+ .mb-6 {
4102
+ margin-bottom: 1.5rem;
4103
+ }
4104
+
4101
4105
  .ml-1 {
4102
4106
  margin-left: 0.25rem;
4103
4107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rovula/ui",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "main": "dist/cjs/bundle.js",
5
5
  "module": "dist/esm/bundle.js",
6
6
  "types": "dist/index.d.ts",
@@ -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
- >(({ className, sideOffset = 4, ...props }, ref) => (
71
- <DropdownMenuPrimitive.Portal>
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 ? "justify-between" : undefined}>
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)]">