@versini/ui-menu 6.3.1 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -30
- package/dist/index.d.ts +2 -2
- package/dist/index.js +47 -24
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -38,8 +38,10 @@ npm install @versini/ui-menu
|
|
|
38
38
|
|
|
39
39
|
### Basic Menu
|
|
40
40
|
|
|
41
|
+
> **Important**: Every `MenuItem` must be wrapped in a `MenuGroup`. Rendering a `MenuItem` outside of a `MenuGroup` will throw an error.
|
|
42
|
+
|
|
41
43
|
```tsx
|
|
42
|
-
import { Menu, MenuItem } from "@versini/ui-menu";
|
|
44
|
+
import { Menu, MenuGroup, MenuItem } from "@versini/ui-menu";
|
|
43
45
|
import { ButtonIcon } from "@versini/ui-button";
|
|
44
46
|
import { IconMenu } from "@versini/ui-icons";
|
|
45
47
|
|
|
@@ -52,9 +54,11 @@ function App() {
|
|
|
52
54
|
</ButtonIcon>
|
|
53
55
|
}
|
|
54
56
|
>
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
<MenuGroup>
|
|
58
|
+
<MenuItem label="Profile" onSelect={() => console.info("Profile")} />
|
|
59
|
+
<MenuItem label="Settings" onSelect={() => console.info("Settings")} />
|
|
60
|
+
<MenuItem label="Logout" onSelect={() => console.info("Logout")} />
|
|
61
|
+
</MenuGroup>
|
|
58
62
|
</Menu>
|
|
59
63
|
);
|
|
60
64
|
}
|
|
@@ -65,7 +69,7 @@ function App() {
|
|
|
65
69
|
### Menu with Icons & Selection
|
|
66
70
|
|
|
67
71
|
```tsx
|
|
68
|
-
import { Menu, MenuItem, MenuSeparator } from "@versini/ui-menu";
|
|
72
|
+
import { Menu, MenuGroup, MenuItem, MenuSeparator } from "@versini/ui-menu";
|
|
69
73
|
import { ButtonIcon } from "@versini/ui-button";
|
|
70
74
|
import {
|
|
71
75
|
IconMenu,
|
|
@@ -86,22 +90,24 @@ function AccountMenu() {
|
|
|
86
90
|
}
|
|
87
91
|
onOpenChange={(o) => console.info("open?", o)}
|
|
88
92
|
>
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
<MenuGroup>
|
|
94
|
+
<MenuItem
|
|
95
|
+
label="Profile"
|
|
96
|
+
icon={<IconUser />}
|
|
97
|
+
onSelect={() => setLast("profile")}
|
|
98
|
+
/>
|
|
99
|
+
<MenuItem
|
|
100
|
+
label="Settings"
|
|
101
|
+
icon={<IconSettings />}
|
|
102
|
+
onSelect={() => setLast("settings")}
|
|
103
|
+
/>
|
|
104
|
+
<MenuSeparator />
|
|
105
|
+
<MenuItem
|
|
106
|
+
label="Logout"
|
|
107
|
+
icon={<IconLogout />}
|
|
108
|
+
onSelect={() => setLast("logout")}
|
|
109
|
+
/>
|
|
110
|
+
</MenuGroup>
|
|
105
111
|
</Menu>
|
|
106
112
|
);
|
|
107
113
|
}
|
|
@@ -147,7 +153,9 @@ function SettingsMenu() {
|
|
|
147
153
|
<MenuItem label="French Teacher" selected={selected === 4} />
|
|
148
154
|
</MenuGroup>
|
|
149
155
|
|
|
150
|
-
<
|
|
156
|
+
<MenuGroup className="mt-2">
|
|
157
|
+
<MenuItem label="About" />
|
|
158
|
+
</MenuGroup>
|
|
151
159
|
</Menu>
|
|
152
160
|
);
|
|
153
161
|
}
|
|
@@ -158,7 +166,7 @@ function SettingsMenu() {
|
|
|
158
166
|
Use `MenuLabel` to add a non-interactive heading inside a menu:
|
|
159
167
|
|
|
160
168
|
```tsx
|
|
161
|
-
import { Menu, MenuItem, MenuLabel } from "@versini/ui-menu";
|
|
169
|
+
import { Menu, MenuGroup, MenuItem, MenuLabel } from "@versini/ui-menu";
|
|
162
170
|
import { ButtonIcon } from "@versini/ui-button";
|
|
163
171
|
import { IconBookSparkles, IconMagic, IconProofread } from "@versini/ui-icons";
|
|
164
172
|
|
|
@@ -171,9 +179,11 @@ function PromptsMenu() {
|
|
|
171
179
|
</ButtonIcon>
|
|
172
180
|
}
|
|
173
181
|
>
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
182
|
+
<MenuGroup>
|
|
183
|
+
<MenuLabel>Prompts</MenuLabel>
|
|
184
|
+
<MenuItem label="Summarize..." icon={<IconMagic />} />
|
|
185
|
+
<MenuItem label="Proofread..." icon={<IconProofread />} />
|
|
186
|
+
</MenuGroup>
|
|
177
187
|
</Menu>
|
|
178
188
|
);
|
|
179
189
|
}
|
|
@@ -205,8 +215,10 @@ function SettingsMenu() {
|
|
|
205
215
|
</ButtonIcon>
|
|
206
216
|
}
|
|
207
217
|
>
|
|
208
|
-
<
|
|
209
|
-
|
|
218
|
+
<MenuGroup>
|
|
219
|
+
<MenuItem label="Profile" />
|
|
220
|
+
<MenuItem label="Statistics" />
|
|
221
|
+
</MenuGroup>
|
|
210
222
|
<MenuSeparator />
|
|
211
223
|
|
|
212
224
|
<MenuSub label="Engines and Personas" icon={<IconSettings />}>
|
|
@@ -240,7 +252,9 @@ function SettingsMenu() {
|
|
|
240
252
|
</MenuGroup>
|
|
241
253
|
</MenuSub>
|
|
242
254
|
|
|
243
|
-
<
|
|
255
|
+
<MenuGroup>
|
|
256
|
+
<MenuItem label="About" />
|
|
257
|
+
</MenuGroup>
|
|
244
258
|
</Menu>
|
|
245
259
|
);
|
|
246
260
|
}
|
|
@@ -293,7 +307,7 @@ function SettingsMenu() {
|
|
|
293
307
|
| `icon` | `React.ReactNode` | - | Icon to display on the left of the label. |
|
|
294
308
|
| `children` | `React.ReactNode` | - | Items to render inside sub-menu. |
|
|
295
309
|
| `disabled` | `boolean` | `false` | Whether the sub-menu is disabled. |
|
|
296
|
-
| `sideOffset` | `number` | `
|
|
310
|
+
| `sideOffset` | `number` | `22` | Offset from sub-menu trigger. |
|
|
297
311
|
|
|
298
312
|
### MenuGroup Props
|
|
299
313
|
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ declare type MenuGroupProps = {
|
|
|
14
14
|
/**
|
|
15
15
|
* The label for the menu group.
|
|
16
16
|
*/
|
|
17
|
-
label
|
|
17
|
+
label?: string;
|
|
18
18
|
/**
|
|
19
19
|
* The children to render inside the menu group.
|
|
20
20
|
*/
|
|
@@ -168,7 +168,7 @@ declare type MenuSubProps = {
|
|
|
168
168
|
disabled?: boolean;
|
|
169
169
|
/**
|
|
170
170
|
* The offset distance from the sub-menu trigger.
|
|
171
|
-
* @default
|
|
171
|
+
* @default 22
|
|
172
172
|
*/
|
|
173
173
|
sideOffset?: number;
|
|
174
174
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-menu v6.
|
|
2
|
+
@versini/ui-menu v6.4.0
|
|
3
3
|
© 2026 gizmette.com
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -14,6 +14,7 @@ import { IconNext } from "@versini/ui-icons";
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
const MenuGroupContext = createContext(false);
|
|
17
18
|
/* v8 ignore start - default context values are fallbacks, never used directly */ const MenuRootContext = createContext({
|
|
18
19
|
closeAll: ()=>{}
|
|
19
20
|
});
|
|
@@ -131,6 +132,7 @@ function getLastEnabledIndex(items) {
|
|
|
131
132
|
/* v8 ignore stop */ case "ArrowDown":
|
|
132
133
|
{
|
|
133
134
|
event.preventDefault();
|
|
135
|
+
event.stopPropagation();
|
|
134
136
|
const nextIndex = getNextEnabledIndex(items, activeIndex, 1);
|
|
135
137
|
setActiveIndex(nextIndex);
|
|
136
138
|
focusItem(nextIndex);
|
|
@@ -139,6 +141,7 @@ function getLastEnabledIndex(items) {
|
|
|
139
141
|
case "ArrowUp":
|
|
140
142
|
{
|
|
141
143
|
event.preventDefault();
|
|
144
|
+
event.stopPropagation();
|
|
142
145
|
const prevIndex = getNextEnabledIndex(items, activeIndex, -1);
|
|
143
146
|
setActiveIndex(prevIndex);
|
|
144
147
|
focusItem(prevIndex);
|
|
@@ -147,6 +150,7 @@ function getLastEnabledIndex(items) {
|
|
|
147
150
|
case "Home":
|
|
148
151
|
{
|
|
149
152
|
event.preventDefault();
|
|
153
|
+
event.stopPropagation();
|
|
150
154
|
const firstIndex = getFirstEnabledIndex(items);
|
|
151
155
|
setActiveIndex(firstIndex);
|
|
152
156
|
focusItem(firstIndex);
|
|
@@ -155,6 +159,7 @@ function getLastEnabledIndex(items) {
|
|
|
155
159
|
case "End":
|
|
156
160
|
{
|
|
157
161
|
event.preventDefault();
|
|
162
|
+
event.stopPropagation();
|
|
158
163
|
const lastIndex = getLastEnabledIndex(items);
|
|
159
164
|
setActiveIndex(lastIndex);
|
|
160
165
|
focusItem(lastIndex);
|
|
@@ -164,6 +169,7 @@ function getLastEnabledIndex(items) {
|
|
|
164
169
|
case " ":
|
|
165
170
|
{
|
|
166
171
|
event.preventDefault();
|
|
172
|
+
event.stopPropagation();
|
|
167
173
|
/* v8 ignore start - activeIndex bounds and disabled guard */ if (activeIndex >= 0 && activeIndex < items.length && !items[activeIndex].disabled) {
|
|
168
174
|
items[activeIndex].element.click();
|
|
169
175
|
}
|
|
@@ -172,6 +178,7 @@ function getLastEnabledIndex(items) {
|
|
|
172
178
|
case "Escape":
|
|
173
179
|
{
|
|
174
180
|
event.preventDefault();
|
|
181
|
+
event.stopPropagation();
|
|
175
182
|
onClose();
|
|
176
183
|
break;
|
|
177
184
|
}
|
|
@@ -181,6 +188,7 @@ function getLastEnabledIndex(items) {
|
|
|
181
188
|
const item = items[activeIndex].element;
|
|
182
189
|
if (item.getAttribute("aria-haspopup") === "menu") {
|
|
183
190
|
event.preventDefault();
|
|
191
|
+
event.stopPropagation();
|
|
184
192
|
onOpenSubMenu(item);
|
|
185
193
|
}
|
|
186
194
|
}
|
|
@@ -190,6 +198,7 @@ function getLastEnabledIndex(items) {
|
|
|
190
198
|
{
|
|
191
199
|
if (isSubMenu && onCloseToParent) {
|
|
192
200
|
event.preventDefault();
|
|
201
|
+
event.stopPropagation();
|
|
193
202
|
onCloseToParent();
|
|
194
203
|
}
|
|
195
204
|
break;
|
|
@@ -197,6 +206,7 @@ function getLastEnabledIndex(items) {
|
|
|
197
206
|
case "Tab":
|
|
198
207
|
{
|
|
199
208
|
event.preventDefault();
|
|
209
|
+
event.stopPropagation();
|
|
200
210
|
onClose();
|
|
201
211
|
break;
|
|
202
212
|
}
|
|
@@ -206,6 +216,7 @@ function getLastEnabledIndex(items) {
|
|
|
206
216
|
// or functional keys such as Shift, Control, Alt, Meta, etc.).
|
|
207
217
|
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
|
|
208
218
|
event.preventDefault();
|
|
219
|
+
event.stopPropagation();
|
|
209
220
|
// Reset the debounce timer.
|
|
210
221
|
if (searchTimeoutRef.current) {
|
|
211
222
|
clearTimeout(searchTimeoutRef.current);
|
|
@@ -339,7 +350,7 @@ const calculatePosition = (triggerRect, menuRect, placement, sideOffset, viewpor
|
|
|
339
350
|
};
|
|
340
351
|
};
|
|
341
352
|
const getMenuItemClasses = ({ isSub })=>{
|
|
342
|
-
return clsx("flex flex-row items-center", "w-full", "m-0 first:mt-0 mt-2 sm:mt-1 px-2 py-1", "rounded-md border border-transparent", "text-left text-base select-none cursor-pointer", "outline-hidden", "disabled:cursor-not-allowed disabled:text-copy-medium", "text-copy-dark", "data-highlighted:bg-surface-
|
|
353
|
+
return clsx("flex flex-row items-center", "w-full", "m-0 first:mt-0 mt-2 sm:mt-1 px-2 py-1", "rounded-md border border-transparent", "text-left text-base select-none cursor-pointer", "outline-hidden", "disabled:cursor-not-allowed disabled:text-copy-medium", "text-copy-dark", "data-highlighted:bg-surface-darker", "data-highlighted:text-copy-light", "in-data-menu-group:text-copy-light", "in-data-menu-group:data-highlighted:bg-surface-darker", "in-data-menu-group:data-highlighted:border-border-medium", {
|
|
343
354
|
"data-[state=open]:bg-surface-darker data-[state=open]:text-copy-light justify-between": isSub,
|
|
344
355
|
"data-disabled:cursor-not-allowed data-disabled:text-copy-medium": !isSub
|
|
345
356
|
});
|
|
@@ -619,26 +630,31 @@ Menu.displayName = "Menu";
|
|
|
619
630
|
|
|
620
631
|
|
|
621
632
|
|
|
633
|
+
|
|
622
634
|
const MenuGroup = ({ children, label, className, icon })=>{
|
|
623
|
-
const groupClass = clsx_0("rounded-md", "p-2", "bg-surface-dark", "text-copy-light", className);
|
|
635
|
+
const groupClass = clsx_0("rounded-md", "p-2", "first:mt-0 mt-2", "bg-surface-dark", "text-copy-light", className);
|
|
624
636
|
const labelContainerClass = clsx_0("pt-2 pb-2", "px-3 sm:px-2", "-mx-1 -mt-1", "flex items-center justify-between", "text-xs text-copy-medium uppercase font-bold", "rounded-t-md");
|
|
625
|
-
return /*#__PURE__*/
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
637
|
+
return /*#__PURE__*/ jsx(MenuGroupContext.Provider, {
|
|
638
|
+
value: true,
|
|
639
|
+
children: /*#__PURE__*/ jsxs("div", {
|
|
640
|
+
role: label ? "group" : undefined,
|
|
641
|
+
className: groupClass,
|
|
642
|
+
"data-menu-group": true,
|
|
643
|
+
"aria-label": label || undefined,
|
|
644
|
+
children: [
|
|
645
|
+
label && /*#__PURE__*/ jsx("div", {
|
|
646
|
+
className: labelContainerClass,
|
|
647
|
+
children: /*#__PURE__*/ jsxs("span", {
|
|
648
|
+
className: "flex items-center gap-1",
|
|
649
|
+
children: [
|
|
650
|
+
icon,
|
|
651
|
+
label
|
|
652
|
+
]
|
|
653
|
+
})
|
|
654
|
+
}),
|
|
655
|
+
children
|
|
656
|
+
]
|
|
657
|
+
})
|
|
642
658
|
});
|
|
643
659
|
};
|
|
644
660
|
MenuGroup.displayName = "MenuGroup";
|
|
@@ -652,6 +668,7 @@ const ITEM_CLASS = getMenuItemClasses({
|
|
|
652
668
|
isSub: false
|
|
653
669
|
});
|
|
654
670
|
const MenuItem = ({ label, disabled, icon, raw = false, children, ignoreClick = false, selected, onSelect, onClick, onFocus, onMouseEnter, ...props })=>{
|
|
671
|
+
const isInsideMenuGroup = useContext(MenuGroupContext);
|
|
655
672
|
const itemRef = useRef(null);
|
|
656
673
|
const { closeAll } = useContext(MenuRootContext);
|
|
657
674
|
const { registerItem, unregisterItem, getItems, activeIndex, setActiveIndex, setOpenSubMenuId } = useContext(MenuContentContext);
|
|
@@ -668,6 +685,9 @@ const MenuItem = ({ label, disabled, icon, raw = false, children, ignoreClick =
|
|
|
668
685
|
registerItem,
|
|
669
686
|
unregisterItem
|
|
670
687
|
]);
|
|
688
|
+
if (!isInsideMenuGroup) {
|
|
689
|
+
throw new Error("MenuItem must be used within a MenuGroup.");
|
|
690
|
+
}
|
|
671
691
|
const getMyIndex = ()=>{
|
|
672
692
|
const items = getItems();
|
|
673
693
|
return items.findIndex((item)=>item.element === itemRef.current);
|
|
@@ -821,7 +841,7 @@ const SUB_CONTENT_CLASS = clsx_0("bg-surface-light", "z-60 rounded-md text-copy-
|
|
|
821
841
|
const SUB_TRIGGER_CLASS = getMenuItemClasses({
|
|
822
842
|
isSub: true
|
|
823
843
|
});
|
|
824
|
-
const MenuSub = ({ label, icon, children, disabled = false, sideOffset =
|
|
844
|
+
const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 22 })=>{
|
|
825
845
|
const subMenuId = useUniqueId("av-menu-sub-");
|
|
826
846
|
const [isSubOpen, setIsSubOpen] = useState(false);
|
|
827
847
|
const [subActiveIndex, setSubActiveIndex] = useState(-1);
|
|
@@ -1029,9 +1049,12 @@ const MenuSub = ({ label, icon, children, disabled = false, sideOffset = 14 })=>
|
|
|
1029
1049
|
popover: "manual",
|
|
1030
1050
|
role: "menu",
|
|
1031
1051
|
className: SUB_CONTENT_CLASS,
|
|
1032
|
-
children: /*#__PURE__*/ jsx(
|
|
1033
|
-
value:
|
|
1034
|
-
children:
|
|
1052
|
+
children: /*#__PURE__*/ jsx(MenuGroupContext.Provider, {
|
|
1053
|
+
value: false,
|
|
1054
|
+
children: /*#__PURE__*/ jsx(MenuContentContext.Provider, {
|
|
1055
|
+
value: subContentContextValue,
|
|
1056
|
+
children: children
|
|
1057
|
+
})
|
|
1035
1058
|
})
|
|
1036
1059
|
})
|
|
1037
1060
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versini/ui-menu",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"sideEffects": [
|
|
50
50
|
"**/*.css"
|
|
51
51
|
],
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "3ab8ea89787f8ff5c6e101bff69e19621d31cf6a"
|
|
53
53
|
}
|