@rovula/ui 0.1.35 → 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;
@@ -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;
@@ -1759,6 +1766,8 @@ type MenuOption = {
1759
1766
  onClick?: () => void;
1760
1767
  /** data-testid attached to the item element */
1761
1768
  testId?: string;
1769
+ /** Custom className applied to the item element */
1770
+ className?: string;
1762
1771
  };
1763
1772
  type MenuItemType = {
1764
1773
  type: "item";
@@ -1808,9 +1817,21 @@ type MenuProps = {
1808
1817
  contentClassName?: string;
1809
1818
  /** data-testid attached to the menu content element */
1810
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;
1811
1832
  };
1812
1833
  declare const Menu: {
1813
- ({ 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;
1814
1835
  displayName: string;
1815
1836
  };
1816
1837
 
@@ -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.35",
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}
@@ -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)]">