@rovula/ui 0.1.35 → 0.1.37
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 +4 -4
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/DropdownMenu/DropdownMenu.d.ts +8 -1
- 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/Dialog/Dialog.js +6 -1
- package/dist/components/Dialog/Dialog.stories.js +32 -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/menu/Menu.d.ts +15 -1
- package/dist/esm/types/patterns/menu/Menu.stories.d.ts +3 -1
- package/dist/index.d.ts +23 -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/Dialog/Dialog.stories.tsx +72 -32
- package/src/components/Dialog/Dialog.tsx +6 -0
- package/src/components/DropdownMenu/DropdownMenu.tsx +10 -3
- 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;
|
|
@@ -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;
|
|
@@ -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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useState } from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import * as yup from "yup";
|
|
4
4
|
import {
|
|
@@ -16,6 +16,7 @@ import { Label } from "../Label/Label";
|
|
|
16
16
|
import { Input } from "../Input/Input";
|
|
17
17
|
import { Field, Form, ValidationHintList, ValidationHintRule } from "../Form";
|
|
18
18
|
import PasswordInput from "../PasswordInput";
|
|
19
|
+
import { Menu } from "@/patterns/menu/Menu";
|
|
19
20
|
|
|
20
21
|
const meta = {
|
|
21
22
|
title: "Components/Dialog",
|
|
@@ -219,36 +220,75 @@ export const FigmaFunctionForm = {
|
|
|
219
220
|
} satisfies StoryObj;
|
|
220
221
|
|
|
221
222
|
export const FigmaFunctionFormWithAction = {
|
|
222
|
-
render: () =>
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
223
|
+
render: () => {
|
|
224
|
+
const [open, setOpen] = useState(false);
|
|
225
|
+
const handleOpenChange = (open: boolean) => {
|
|
226
|
+
setOpen(open);
|
|
227
|
+
};
|
|
228
|
+
const handleCancel = () => {
|
|
229
|
+
setOpen(false);
|
|
230
|
+
};
|
|
231
|
+
const handleConfirm = () => {
|
|
232
|
+
setOpen(false);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div className="flex w-full">
|
|
237
|
+
<Menu
|
|
238
|
+
items={[
|
|
239
|
+
{
|
|
240
|
+
type: "item",
|
|
241
|
+
item: {
|
|
242
|
+
value: "edit",
|
|
243
|
+
label: "Edit",
|
|
244
|
+
onClick: () => handleOpenChange(true),
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
type: "item",
|
|
249
|
+
item: {
|
|
250
|
+
value: "delete",
|
|
251
|
+
label: "Delete",
|
|
252
|
+
onClick: () => handleOpenChange(true),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
]}
|
|
256
|
+
trigger={<Button variant="outline">Open</Button>}
|
|
257
|
+
/>
|
|
258
|
+
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
259
|
+
<DialogContent>
|
|
260
|
+
<DialogHeader>
|
|
261
|
+
<DialogTitle>Title</DialogTitle>
|
|
262
|
+
<DialogDescription>Subtitle description</DialogDescription>
|
|
263
|
+
</DialogHeader>
|
|
264
|
+
<DialogBody>
|
|
265
|
+
<div className="flex items-center justify-center bg-ramps-secondary-150 h-[200px] w-full rounded-sm">
|
|
266
|
+
<p className="typography-body3 text-text-contrast-max">
|
|
267
|
+
Content - Form Area
|
|
268
|
+
</p>
|
|
269
|
+
</div>
|
|
270
|
+
</DialogBody>
|
|
271
|
+
<DialogFooter className="justify-between">
|
|
272
|
+
<Button variant="outline" color="secondary" fullwidth={false}>
|
|
273
|
+
Medium
|
|
247
274
|
</Button>
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
275
|
+
<div className="flex items-center gap-4">
|
|
276
|
+
<Button
|
|
277
|
+
variant="outline"
|
|
278
|
+
color="primary"
|
|
279
|
+
fullwidth={false}
|
|
280
|
+
onClick={handleCancel}
|
|
281
|
+
>
|
|
282
|
+
Cancel
|
|
283
|
+
</Button>
|
|
284
|
+
<Button disabled fullwidth={false} onClick={handleConfirm}>
|
|
285
|
+
Confirm
|
|
286
|
+
</Button>
|
|
287
|
+
</div>
|
|
288
|
+
</DialogFooter>
|
|
289
|
+
</DialogContent>
|
|
290
|
+
</Dialog>
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
},
|
|
254
294
|
} satisfies StoryObj;
|
|
@@ -55,6 +55,12 @@ const DialogContent = React.forwardRef<
|
|
|
55
55
|
className,
|
|
56
56
|
)}
|
|
57
57
|
{...props}
|
|
58
|
+
onCloseAutoFocus={(event) => {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
document.body.style.pointerEvents = "auto";
|
|
61
|
+
|
|
62
|
+
props?.onCloseAutoFocus?.(event);
|
|
63
|
+
}}
|
|
58
64
|
>
|
|
59
65
|
{children}
|
|
60
66
|
{showCloseButton && (
|
|
@@ -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}
|
|
@@ -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)]">
|