@malilion/block-ui-react 0.4.0 → 0.5.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/dist/components/actions/BlockMenu/BlockMenu.d.ts.map +1 -1
- package/dist/components/actions/BlockMenu/BlockMenu.js +17 -117
- package/dist/components/actions/BlockMenu/BlockMenu.js.map +1 -1
- package/dist/components/actions/BlockMenu/BlockMenu.utils.d.ts +2 -0
- package/dist/components/actions/BlockMenu/BlockMenu.utils.d.ts.map +1 -1
- package/dist/components/actions/BlockMenu/BlockMenu.utils.js +5 -1
- package/dist/components/actions/BlockMenu/BlockMenu.utils.js.map +1 -1
- package/dist/components/actions/BlockMenu/MenuList.d.ts +28 -0
- package/dist/components/actions/BlockMenu/MenuList.d.ts.map +1 -0
- package/dist/components/actions/BlockMenu/MenuList.js +134 -0
- package/dist/components/actions/BlockMenu/MenuList.js.map +1 -0
- package/dist/components/actions/ContextMenu/ContextMenu.d.ts +8 -0
- package/dist/components/actions/ContextMenu/ContextMenu.d.ts.map +1 -0
- package/dist/components/actions/ContextMenu/ContextMenu.js +100 -0
- package/dist/components/actions/ContextMenu/ContextMenu.js.map +1 -0
- package/dist/components/actions/ContextMenu/ContextMenu.module.js +7 -0
- package/dist/components/actions/ContextMenu/ContextMenu.module.js.map +1 -0
- package/dist/components/actions/ContextMenu/ContextMenu.types.d.ts +15 -0
- package/dist/components/actions/ContextMenu/ContextMenu.types.d.ts.map +1 -0
- package/dist/components/actions/ContextMenu/index.d.ts +3 -0
- package/dist/components/actions/ContextMenu/index.d.ts.map +1 -0
- package/dist/components/actions/index.d.ts +1 -0
- package/dist/components/actions/index.d.ts.map +1 -1
- package/dist/components/crafting/RecipeBook/RecipeBook.js +2 -2
- package/dist/components/crafting/RecipeBook/RecipeBook.js.map +1 -1
- package/dist/components/display/Kbd/Kbd.d.ts +4 -0
- package/dist/components/display/Kbd/Kbd.d.ts.map +1 -0
- package/dist/components/display/Kbd/Kbd.js +33 -0
- package/dist/components/display/Kbd/Kbd.js.map +1 -0
- package/dist/components/display/Kbd/Kbd.module.js +13 -0
- package/dist/components/display/Kbd/Kbd.module.js.map +1 -0
- package/dist/components/display/Kbd/Kbd.types.d.ts +9 -0
- package/dist/components/display/Kbd/Kbd.types.d.ts.map +1 -0
- package/dist/components/display/Kbd/index.d.ts +3 -0
- package/dist/components/display/Kbd/index.d.ts.map +1 -0
- package/dist/components/display/Popover/Popover.d.ts +8 -0
- package/dist/components/display/Popover/Popover.d.ts.map +1 -0
- package/dist/components/display/Popover/Popover.js +101 -0
- package/dist/components/display/Popover/Popover.js.map +1 -0
- package/dist/components/display/Popover/Popover.module.js +15 -0
- package/dist/components/display/Popover/Popover.module.js.map +1 -0
- package/dist/components/display/Popover/Popover.types.d.ts +26 -0
- package/dist/components/display/Popover/Popover.types.d.ts.map +1 -0
- package/dist/components/display/Popover/index.d.ts +3 -0
- package/dist/components/display/Popover/index.d.ts.map +1 -0
- package/dist/components/display/SkillTree/SkillTree.js +2 -2
- package/dist/components/display/SkillTree/SkillTree.js.map +1 -1
- package/dist/components/display/index.d.ts +2 -0
- package/dist/components/display/index.d.ts.map +1 -1
- package/dist/components/feedback/ConfirmDialog/ConfirmDialog.js +2 -2
- package/dist/components/feedback/ConfirmDialog/ConfirmDialog.js.map +1 -1
- package/dist/components/inventory/InventorySlot/InventorySlot.js +2 -2
- package/dist/components/inventory/InventorySlot/InventorySlot.js.map +1 -1
- package/dist/components/layout/BlockContainer/BlockContainer.d.ts +4 -0
- package/dist/components/layout/BlockContainer/BlockContainer.d.ts.map +1 -0
- package/dist/components/layout/BlockContainer/BlockContainer.js +18 -0
- package/dist/components/layout/BlockContainer/BlockContainer.js.map +1 -0
- package/dist/components/layout/BlockContainer/BlockContainer.module.js +7 -0
- package/dist/components/layout/BlockContainer/BlockContainer.module.js.map +1 -0
- package/dist/components/layout/BlockContainer/BlockContainer.types.d.ts +11 -0
- package/dist/components/layout/BlockContainer/BlockContainer.types.d.ts.map +1 -0
- package/dist/components/layout/BlockContainer/index.d.ts +3 -0
- package/dist/components/layout/BlockContainer/index.d.ts.map +1 -0
- package/dist/components/layout/BlockGrid/BlockGrid.d.ts +4 -0
- package/dist/components/layout/BlockGrid/BlockGrid.d.ts.map +1 -0
- package/dist/components/layout/BlockGrid/BlockGrid.js +25 -0
- package/dist/components/layout/BlockGrid/BlockGrid.js.map +1 -0
- package/dist/components/layout/BlockGrid/BlockGrid.module.js +7 -0
- package/dist/components/layout/BlockGrid/BlockGrid.module.js.map +1 -0
- package/dist/components/layout/BlockGrid/BlockGrid.types.d.ts +16 -0
- package/dist/components/layout/BlockGrid/BlockGrid.types.d.ts.map +1 -0
- package/dist/components/layout/BlockGrid/index.d.ts +3 -0
- package/dist/components/layout/BlockGrid/index.d.ts.map +1 -0
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/components/navigation/BlockSidebar/BlockSidebar.js +3 -3
- package/dist/components/navigation/BlockSidebar/BlockSidebar.js.map +1 -1
- package/dist/components/navigation/BlockStepper/BlockStepper.d.ts +8 -0
- package/dist/components/navigation/BlockStepper/BlockStepper.d.ts.map +1 -0
- package/dist/components/navigation/BlockStepper/BlockStepper.js +66 -0
- package/dist/components/navigation/BlockStepper/BlockStepper.js.map +1 -0
- package/dist/components/navigation/BlockStepper/BlockStepper.module.js +23 -0
- package/dist/components/navigation/BlockStepper/BlockStepper.module.js.map +1 -0
- package/dist/components/navigation/BlockStepper/BlockStepper.types.d.ts +19 -0
- package/dist/components/navigation/BlockStepper/BlockStepper.types.d.ts.map +1 -0
- package/dist/components/navigation/BlockStepper/index.d.ts +3 -0
- package/dist/components/navigation/BlockStepper/index.d.ts.map +1 -0
- package/dist/components/navigation/Breadcrumb/Breadcrumb.js +2 -2
- package/dist/components/navigation/Breadcrumb/Breadcrumb.js.map +1 -1
- package/dist/components/navigation/HotbarNavigation/HotbarNavigation.js +3 -3
- package/dist/components/navigation/HotbarNavigation/HotbarNavigation.js.map +1 -1
- package/dist/components/navigation/index.d.ts +1 -0
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/hooks/useAnchoredPosition.d.ts +3 -1
- package/dist/hooks/useAnchoredPosition.d.ts.map +1 -1
- package/dist/hooks/useAnchoredPosition.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/styles.css +281 -0
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BlockMenu.d.ts","sourceRoot":"","sources":["../../../../src/components/actions/BlockMenu/BlockMenu.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"BlockMenu.d.ts","sourceRoot":"","sources":["../../../../src/components/actions/BlockMenu/BlockMenu.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAIvE;;;;;GAKG;AACH,eAAO,MAAM,SAAS,2GA0HpB,CAAC"}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { cx } from "../../../utils/cx.js";
|
|
2
|
-
import { usePortalContainer } from "../../../provider/context.js";
|
|
3
2
|
import { BlockButton } from "../BlockButton/BlockButton.js";
|
|
4
3
|
import IconButton_module_default from "../IconButton/IconButton.module.js";
|
|
5
4
|
import { useAnchoredPosition } from "../../../hooks/useAnchoredPosition.js";
|
|
6
5
|
import { mergeRefs } from "../../../utils/refs.js";
|
|
7
6
|
import BlockMenu_module_default from "./BlockMenu.module.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
7
|
+
import { enabledMenuIndexes } from "./BlockMenu.utils.js";
|
|
8
|
+
import { MenuList } from "./MenuList.js";
|
|
9
|
+
import { forwardRef, useId, useRef, useState } from "react";
|
|
11
10
|
import { ChevronDownIcon } from "@malilion/block-ui-icons";
|
|
12
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
12
|
//#region src/components/actions/BlockMenu/BlockMenu.tsx
|
|
@@ -21,11 +20,9 @@ var BlockMenu = forwardRef(function BlockMenu({ label, items, onSelect, variant
|
|
|
21
20
|
const baseId = useId();
|
|
22
21
|
const triggerId = `${baseId}-trigger`;
|
|
23
22
|
const menuId = `${baseId}-menu`;
|
|
24
|
-
const container = usePortalContainer();
|
|
25
23
|
const rootRef = useRef(null);
|
|
26
24
|
const menuRef = useRef(null);
|
|
27
25
|
const triggerRef = useRef(null);
|
|
28
|
-
const itemRefs = useRef(/* @__PURE__ */ new Map());
|
|
29
26
|
const [open, setOpenState] = useState(false);
|
|
30
27
|
const [focusTarget, setFocusTarget] = useState(null);
|
|
31
28
|
const position = useAnchoredPosition(triggerRef, menuRef, open, {
|
|
@@ -34,14 +31,12 @@ var BlockMenu = forwardRef(function BlockMenu({ label, items, onSelect, variant
|
|
|
34
31
|
gap: 4
|
|
35
32
|
});
|
|
36
33
|
const isInside = (node) => Boolean(node && (rootRef.current?.contains(node) || menuRef.current?.contains(node)));
|
|
37
|
-
const menuItems = items.filter(isMenuItem);
|
|
38
|
-
const enabledIndexes = menuItems.flatMap((item, index) => item.disabled ? [] : [index]);
|
|
39
34
|
const setOpen = (next) => {
|
|
40
35
|
setOpenState(next);
|
|
41
36
|
onOpenChange?.(next);
|
|
42
37
|
};
|
|
43
38
|
const openMenu = (target) => {
|
|
44
|
-
if (disabled ||
|
|
39
|
+
if (disabled || enabledMenuIndexes(items).length === 0) return;
|
|
45
40
|
setOpen(true);
|
|
46
41
|
setFocusTarget(target);
|
|
47
42
|
};
|
|
@@ -50,29 +45,7 @@ var BlockMenu = forwardRef(function BlockMenu({ label, items, onSelect, variant
|
|
|
50
45
|
setFocusTarget(null);
|
|
51
46
|
if (returnFocus) triggerRef.current?.focus();
|
|
52
47
|
};
|
|
53
|
-
const focusItem = (index) => {
|
|
54
|
-
const item = menuItems[index];
|
|
55
|
-
if (item) itemRefs.current.get(item.id)?.focus();
|
|
56
|
-
};
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
if (!open || focusTarget === null || !position) return;
|
|
59
|
-
const index = focusTarget === "first" ? enabledIndexes[0] : focusTarget === "last" ? enabledIndexes[enabledIndexes.length - 1] : focusTarget;
|
|
60
|
-
if (index !== void 0) focusItem(index);
|
|
61
|
-
}, [
|
|
62
|
-
open,
|
|
63
|
-
focusTarget,
|
|
64
|
-
position !== null
|
|
65
|
-
]);
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
if (!open) return;
|
|
68
|
-
const onPointerDown = (event) => {
|
|
69
|
-
if (!isInside(event.target)) closeMenu(false);
|
|
70
|
-
};
|
|
71
|
-
document.addEventListener("pointerdown", onPointerDown);
|
|
72
|
-
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
73
|
-
}, [open]);
|
|
74
48
|
const choose = (item) => {
|
|
75
|
-
if (item.disabled) return;
|
|
76
49
|
closeMenu(true);
|
|
77
50
|
item.onSelect?.();
|
|
78
51
|
onSelect?.(item.id);
|
|
@@ -86,44 +59,6 @@ var BlockMenu = forwardRef(function BlockMenu({ label, items, onSelect, variant
|
|
|
86
59
|
openMenu("last");
|
|
87
60
|
}
|
|
88
61
|
};
|
|
89
|
-
const onMenuKeyDown = (event) => {
|
|
90
|
-
const current = menuItems.findIndex((item) => itemRefs.current.get(item.id) === document.activeElement);
|
|
91
|
-
const position = enabledIndexes.indexOf(current);
|
|
92
|
-
const count = enabledIndexes.length;
|
|
93
|
-
let next;
|
|
94
|
-
switch (event.key) {
|
|
95
|
-
case "ArrowDown":
|
|
96
|
-
next = enabledIndexes[(position + 1) % count];
|
|
97
|
-
break;
|
|
98
|
-
case "ArrowUp":
|
|
99
|
-
next = enabledIndexes[(position - 1 + count) % count];
|
|
100
|
-
break;
|
|
101
|
-
case "Home":
|
|
102
|
-
next = enabledIndexes[0];
|
|
103
|
-
break;
|
|
104
|
-
case "End":
|
|
105
|
-
next = enabledIndexes[count - 1];
|
|
106
|
-
break;
|
|
107
|
-
case "Escape":
|
|
108
|
-
event.preventDefault();
|
|
109
|
-
event.stopPropagation();
|
|
110
|
-
closeMenu(true);
|
|
111
|
-
return;
|
|
112
|
-
case "Tab":
|
|
113
|
-
closeMenu(true);
|
|
114
|
-
return;
|
|
115
|
-
default: if (event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey) {
|
|
116
|
-
const match = typeaheadIndex(menuItems, current, event.key);
|
|
117
|
-
if (match >= 0) next = match;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (next === void 0) return;
|
|
121
|
-
event.preventDefault();
|
|
122
|
-
focusItem(next);
|
|
123
|
-
};
|
|
124
|
-
const onMenuBlur = (event) => {
|
|
125
|
-
if (!isInside(event.relatedTarget)) closeMenu(false);
|
|
126
|
-
};
|
|
127
62
|
return /* @__PURE__ */ jsxs("div", {
|
|
128
63
|
ref: mergeRefs(ref, rootRef),
|
|
129
64
|
className: cx(BlockMenu_module_default.root, className),
|
|
@@ -154,55 +89,20 @@ var BlockMenu = forwardRef(function BlockMenu({ label, items, onSelect, variant
|
|
|
154
89
|
"aria-hidden": "true",
|
|
155
90
|
children: icon
|
|
156
91
|
}) : label
|
|
157
|
-
}),
|
|
158
|
-
ref: menuRef,
|
|
92
|
+
}), /* @__PURE__ */ jsx(MenuList, {
|
|
159
93
|
id: menuId,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
role: "separator",
|
|
173
|
-
className: BlockMenu_module_default.separator
|
|
174
|
-
}, entry.id ?? `separator-${index}`);
|
|
175
|
-
return /* @__PURE__ */ jsxs("button", {
|
|
176
|
-
ref: (element) => {
|
|
177
|
-
if (element) itemRefs.current.set(entry.id, element);
|
|
178
|
-
else itemRefs.current.delete(entry.id);
|
|
179
|
-
},
|
|
180
|
-
type: "button",
|
|
181
|
-
role: "menuitem",
|
|
182
|
-
tabIndex: -1,
|
|
183
|
-
"aria-disabled": entry.disabled || void 0,
|
|
184
|
-
"data-danger": entry.danger || void 0,
|
|
185
|
-
className: BlockMenu_module_default.item,
|
|
186
|
-
onClick: () => choose(entry),
|
|
187
|
-
children: [
|
|
188
|
-
/* @__PURE__ */ jsx("span", {
|
|
189
|
-
className: BlockMenu_module_default.icon,
|
|
190
|
-
"aria-hidden": "true",
|
|
191
|
-
children: entry.icon
|
|
192
|
-
}),
|
|
193
|
-
/* @__PURE__ */ jsx("span", {
|
|
194
|
-
className: BlockMenu_module_default.label,
|
|
195
|
-
children: entry.label
|
|
196
|
-
}),
|
|
197
|
-
entry.shortcut ? /* @__PURE__ */ jsx("kbd", {
|
|
198
|
-
className: BlockMenu_module_default.shortcut,
|
|
199
|
-
"aria-hidden": "true",
|
|
200
|
-
children: entry.shortcut
|
|
201
|
-
}) : null
|
|
202
|
-
]
|
|
203
|
-
}, entry.id);
|
|
204
|
-
})
|
|
205
|
-
}), container) : null]
|
|
94
|
+
items,
|
|
95
|
+
open,
|
|
96
|
+
focusTarget,
|
|
97
|
+
menuRef,
|
|
98
|
+
position,
|
|
99
|
+
labelledBy: triggerId,
|
|
100
|
+
align,
|
|
101
|
+
placement,
|
|
102
|
+
isInside,
|
|
103
|
+
onClose: closeMenu,
|
|
104
|
+
onChoose: choose
|
|
105
|
+
})]
|
|
206
106
|
});
|
|
207
107
|
});
|
|
208
108
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BlockMenu.js","names":["styles","iconButtonStyles"],"sources":["../../../../src/components/actions/BlockMenu/BlockMenu.tsx"],"sourcesContent":["import { ChevronDownIcon } from \"@malilion/block-ui-icons\";\nimport {\n forwardRef,\n useEffect,\n useId,\n useRef,\n useState,\n type FocusEvent,\n type KeyboardEvent,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useAnchoredPosition } from \"../../../hooks/useAnchoredPosition\";\nimport { usePortalContainer } from \"../../../provider/context\";\nimport { cx } from \"../../../utils/cx\";\nimport { mergeRefs } from \"../../../utils/refs\";\nimport { BlockButton } from \"../BlockButton/BlockButton\";\nimport iconButtonStyles from \"../IconButton/IconButton.module.css\";\nimport styles from \"./BlockMenu.module.css\";\nimport type { BlockMenuItem, BlockMenuProps } from \"./BlockMenu.types\";\nimport { isMenuItem, typeaheadIndex } from \"./BlockMenu.utils\";\n\ntype FocusTarget = \"first\" | \"last\" | number;\n\n/**\n * Dropdown menu button following the WAI-ARIA menu button pattern: arrow keys\n * move with wrap-around, Home/End jump, letters type-ahead, Enter/Space choose\n * and Escape closes — focus always returns to the trigger. The menu renders in\n * the provider's overlay layer, so scroll containers (tables) never clip it.\n */\nexport const BlockMenu = forwardRef<HTMLDivElement, BlockMenuProps>(function BlockMenu(\n {\n label,\n items,\n onSelect,\n variant = \"stone\",\n size = \"md\",\n icon,\n iconOnly = false,\n align = \"start\",\n placement = \"bottom\",\n disabled = false,\n onOpenChange,\n className,\n ...rest\n },\n ref,\n) {\n const baseId = useId();\n const triggerId = `${baseId}-trigger`;\n const menuId = `${baseId}-menu`;\n const container = usePortalContainer();\n const rootRef = useRef<HTMLDivElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const itemRefs = useRef(new Map<string, HTMLButtonElement>());\n const [open, setOpenState] = useState(false);\n const [focusTarget, setFocusTarget] = useState<FocusTarget | null>(null);\n\n const position = useAnchoredPosition(triggerRef, menuRef, open, {\n side: placement,\n align: align === \"end\" ? \"end\" : \"start\",\n gap: 4,\n });\n const isInside = (node: Node | null) =>\n Boolean(node && (rootRef.current?.contains(node) || menuRef.current?.contains(node)));\n\n const menuItems = items.filter(isMenuItem);\n const enabledIndexes = menuItems.flatMap((item, index) => (item.disabled ? [] : [index]));\n\n const setOpen = (next: boolean) => {\n setOpenState(next);\n onOpenChange?.(next);\n };\n\n const openMenu = (target: FocusTarget) => {\n if (disabled || enabledIndexes.length === 0) return;\n setOpen(true);\n setFocusTarget(target);\n };\n\n const closeMenu = (returnFocus: boolean) => {\n setOpen(false);\n setFocusTarget(null);\n if (returnFocus) triggerRef.current?.focus();\n };\n\n const focusItem = (index: number) => {\n const item = menuItems[index];\n if (item) itemRefs.current.get(item.id)?.focus();\n };\n\n useEffect(() => {\n if (!open || focusTarget === null || !position) return;\n const index =\n focusTarget === \"first\"\n ? enabledIndexes[0]\n : focusTarget === \"last\"\n ? enabledIndexes[enabledIndexes.length - 1]\n : focusTarget;\n if (index !== undefined) focusItem(index);\n // Focus once the menu is open and positioned, or when the target changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, focusTarget, position !== null]);\n\n useEffect(() => {\n if (!open) return;\n const onPointerDown = (event: PointerEvent) => {\n if (!isInside(event.target as Node)) closeMenu(false);\n };\n document.addEventListener(\"pointerdown\", onPointerDown);\n return () => document.removeEventListener(\"pointerdown\", onPointerDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n const choose = (item: BlockMenuItem) => {\n if (item.disabled) return;\n closeMenu(true);\n item.onSelect?.();\n onSelect?.(item.id);\n };\n\n const onTriggerKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n if (event.key === \"ArrowDown\" || event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n openMenu(\"first\");\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n openMenu(\"last\");\n }\n };\n\n const onMenuKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n const current = menuItems.findIndex(\n (item) => itemRefs.current.get(item.id) === document.activeElement,\n );\n const position = enabledIndexes.indexOf(current);\n const count = enabledIndexes.length;\n let next: number | undefined;\n\n switch (event.key) {\n case \"ArrowDown\":\n next = enabledIndexes[(position + 1) % count];\n break;\n case \"ArrowUp\":\n next = enabledIndexes[(position - 1 + count) % count];\n break;\n case \"Home\":\n next = enabledIndexes[0];\n break;\n case \"End\":\n next = enabledIndexes[count - 1];\n break;\n case \"Escape\":\n event.preventDefault();\n event.stopPropagation();\n closeMenu(true);\n return;\n case \"Tab\":\n // The menu lives in the overlay layer at the end of the page: hand focus back to the\n // trigger first so the browser's Tab continues from the trigger's place in the page.\n closeMenu(true);\n return;\n default:\n if (event.key.length === 1 && /\\S/.test(event.key) && !event.ctrlKey && !event.metaKey) {\n const match = typeaheadIndex(menuItems, current, event.key);\n if (match >= 0) next = match;\n }\n }\n if (next === undefined) return;\n event.preventDefault();\n focusItem(next);\n };\n\n const onMenuBlur = (event: FocusEvent<HTMLDivElement>) => {\n if (!isInside(event.relatedTarget as Node | null)) closeMenu(false);\n };\n\n return (\n <div\n ref={mergeRefs(ref, rootRef)}\n className={cx(styles.root, className)}\n data-open={open || undefined}\n {...rest}\n >\n <BlockButton\n ref={triggerRef}\n id={triggerId}\n variant={variant}\n size={size}\n disabled={disabled}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={menuId}\n aria-label={iconOnly ? label : undefined}\n title={iconOnly ? label : undefined}\n startIcon={iconOnly ? undefined : icon}\n endIcon={\n iconOnly ? undefined : (\n <span className={styles.chevron} aria-hidden=\"true\">\n <ChevronDownIcon size={16} />\n </span>\n )\n }\n className={cx(iconOnly && iconButtonStyles.iconButton)}\n onClick={() => (open ? closeMenu(false) : openMenu(\"first\"))}\n onKeyDown={onTriggerKeyDown}\n >\n {iconOnly ? (\n <span className={iconButtonStyles.icon} aria-hidden=\"true\">\n {icon}\n </span>\n ) : (\n label\n )}\n </BlockButton>\n {container\n ? createPortal(\n <div\n ref={menuRef}\n id={menuId}\n role=\"menu\"\n tabIndex={-1}\n aria-labelledby={triggerId}\n hidden={!open}\n data-align={align}\n data-placement={position?.side ?? placement}\n className={styles.menu}\n style={position?.style}\n onKeyDown={onMenuKeyDown}\n onBlur={onMenuBlur}\n >\n {items.map((entry, index) => {\n if (!isMenuItem(entry)) {\n return (\n <div\n key={entry.id ?? `separator-${index}`}\n role=\"separator\"\n className={styles.separator}\n />\n );\n }\n return (\n <button\n key={entry.id}\n ref={(element) => {\n if (element) itemRefs.current.set(entry.id, element);\n else itemRefs.current.delete(entry.id);\n }}\n type=\"button\"\n role=\"menuitem\"\n tabIndex={-1}\n aria-disabled={entry.disabled || undefined}\n data-danger={entry.danger || undefined}\n className={styles.item}\n onClick={() => choose(entry)}\n >\n <span className={styles.icon} aria-hidden=\"true\">\n {entry.icon}\n </span>\n <span className={styles.label}>{entry.label}</span>\n {entry.shortcut ? (\n <kbd className={styles.shortcut} aria-hidden=\"true\">\n {entry.shortcut}\n </kbd>\n ) : null}\n </button>\n );\n })}\n </div>,\n container,\n )\n : null}\n </div>\n );\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BlockMenu.js","names":["styles","iconButtonStyles"],"sources":["../../../../src/components/actions/BlockMenu/BlockMenu.tsx"],"sourcesContent":["import { ChevronDownIcon } from \"@malilion/block-ui-icons\";\nimport { forwardRef, useId, useRef, useState, type KeyboardEvent } from \"react\";\nimport { useAnchoredPosition } from \"../../../hooks/useAnchoredPosition\";\nimport { cx } from \"../../../utils/cx\";\nimport { mergeRefs } from \"../../../utils/refs\";\nimport { BlockButton } from \"../BlockButton/BlockButton\";\nimport iconButtonStyles from \"../IconButton/IconButton.module.css\";\nimport styles from \"./BlockMenu.module.css\";\nimport type { BlockMenuItem, BlockMenuProps } from \"./BlockMenu.types\";\nimport { enabledMenuIndexes } from \"./BlockMenu.utils\";\nimport { MenuList, type MenuFocusTarget } from \"./MenuList\";\n\n/**\n * Dropdown menu button following the WAI-ARIA menu button pattern: arrow keys\n * move with wrap-around, Home/End jump, letters type-ahead, Enter/Space choose\n * and Escape closes — focus always returns to the trigger. The menu renders in\n * the provider's overlay layer, so scroll containers (tables) never clip it.\n */\nexport const BlockMenu = forwardRef<HTMLDivElement, BlockMenuProps>(function BlockMenu(\n {\n label,\n items,\n onSelect,\n variant = \"stone\",\n size = \"md\",\n icon,\n iconOnly = false,\n align = \"start\",\n placement = \"bottom\",\n disabled = false,\n onOpenChange,\n className,\n ...rest\n },\n ref,\n) {\n const baseId = useId();\n const triggerId = `${baseId}-trigger`;\n const menuId = `${baseId}-menu`;\n const rootRef = useRef<HTMLDivElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const [open, setOpenState] = useState(false);\n const [focusTarget, setFocusTarget] = useState<MenuFocusTarget | null>(null);\n\n const position = useAnchoredPosition(triggerRef, menuRef, open, {\n side: placement,\n align: align === \"end\" ? \"end\" : \"start\",\n gap: 4,\n });\n const isInside = (node: Node | null) =>\n Boolean(node && (rootRef.current?.contains(node) || menuRef.current?.contains(node)));\n\n const setOpen = (next: boolean) => {\n setOpenState(next);\n onOpenChange?.(next);\n };\n\n const openMenu = (target: MenuFocusTarget) => {\n if (disabled || enabledMenuIndexes(items).length === 0) return;\n setOpen(true);\n setFocusTarget(target);\n };\n\n const closeMenu = (returnFocus: boolean) => {\n setOpen(false);\n setFocusTarget(null);\n if (returnFocus) triggerRef.current?.focus();\n };\n\n const choose = (item: BlockMenuItem) => {\n closeMenu(true);\n item.onSelect?.();\n onSelect?.(item.id);\n };\n\n const onTriggerKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n if (event.key === \"ArrowDown\" || event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n openMenu(\"first\");\n } else if (event.key === \"ArrowUp\") {\n event.preventDefault();\n openMenu(\"last\");\n }\n };\n\n return (\n <div\n ref={mergeRefs(ref, rootRef)}\n className={cx(styles.root, className)}\n data-open={open || undefined}\n {...rest}\n >\n <BlockButton\n ref={triggerRef}\n id={triggerId}\n variant={variant}\n size={size}\n disabled={disabled}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n aria-controls={menuId}\n aria-label={iconOnly ? label : undefined}\n title={iconOnly ? label : undefined}\n startIcon={iconOnly ? undefined : icon}\n endIcon={\n iconOnly ? undefined : (\n <span className={styles.chevron} aria-hidden=\"true\">\n <ChevronDownIcon size={16} />\n </span>\n )\n }\n className={cx(iconOnly && iconButtonStyles.iconButton)}\n onClick={() => (open ? closeMenu(false) : openMenu(\"first\"))}\n onKeyDown={onTriggerKeyDown}\n >\n {iconOnly ? (\n <span className={iconButtonStyles.icon} aria-hidden=\"true\">\n {icon}\n </span>\n ) : (\n label\n )}\n </BlockButton>\n <MenuList\n id={menuId}\n items={items}\n open={open}\n focusTarget={focusTarget}\n menuRef={menuRef}\n position={position}\n labelledBy={triggerId}\n align={align}\n placement={placement}\n isInside={isInside}\n onClose={closeMenu}\n onChoose={choose}\n />\n </div>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkBA,IAAa,YAAY,WAA2C,SAAS,UAC3E,EACE,OACA,OACA,UACA,UAAU,SACV,OAAO,MACP,MACA,WAAW,OACX,QAAQ,SACR,YAAY,UACZ,WAAW,OACX,cACA,WACA,GAAG,QAEL,KACA;CACA,MAAM,SAAS,MAAM;CACrB,MAAM,YAAY,GAAG,OAAO;CAC5B,MAAM,SAAS,GAAG,OAAO;CACzB,MAAM,UAAU,OAAuB,IAAI;CAC3C,MAAM,UAAU,OAAuB,IAAI;CAC3C,MAAM,aAAa,OAA0B,IAAI;CACjD,MAAM,CAAC,MAAM,gBAAgB,SAAS,KAAK;CAC3C,MAAM,CAAC,aAAa,kBAAkB,SAAiC,IAAI;CAE3E,MAAM,WAAW,oBAAoB,YAAY,SAAS,MAAM;EAC9D,MAAM;EACN,OAAO,UAAU,QAAQ,QAAQ;EACjC,KAAK;CACP,CAAC;CACD,MAAM,YAAY,SAChB,QAAQ,SAAS,QAAQ,SAAS,SAAS,IAAI,KAAK,QAAQ,SAAS,SAAS,IAAI,EAAE;CAEtF,MAAM,WAAW,SAAkB;EACjC,aAAa,IAAI;EACjB,eAAe,IAAI;CACrB;CAEA,MAAM,YAAY,WAA4B;EAC5C,IAAI,YAAY,mBAAmB,KAAK,CAAC,CAAC,WAAW,GAAG;EACxD,QAAQ,IAAI;EACZ,eAAe,MAAM;CACvB;CAEA,MAAM,aAAa,gBAAyB;EAC1C,QAAQ,KAAK;EACb,eAAe,IAAI;EACnB,IAAI,aAAa,WAAW,SAAS,MAAM;CAC7C;CAEA,MAAM,UAAU,SAAwB;EACtC,UAAU,IAAI;EACd,KAAK,WAAW;EAChB,WAAW,KAAK,EAAE;CACpB;CAEA,MAAM,oBAAoB,UAA4C;EACpE,IAAI,MAAM,QAAQ,eAAe,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK;GAC3E,MAAM,eAAe;GACrB,SAAS,OAAO;EAClB,OAAO,IAAI,MAAM,QAAQ,WAAW;GAClC,MAAM,eAAe;GACrB,SAAS,MAAM;EACjB;CACF;CAEA,OACE,qBAAC,OAAD;EACE,KAAK,UAAU,KAAK,OAAO;EAC3B,WAAW,GAAGA,yBAAO,MAAM,SAAS;EACpC,aAAW,QAAQ,KAAA;EACnB,GAAI;EAJN,UAAA,CAME,oBAAC,aAAD;GACE,KAAK;GACL,IAAI;GACK;GACH;GACI;GACV,iBAAc;GACd,iBAAe;GACf,iBAAe;GACf,cAAY,WAAW,QAAQ,KAAA;GAC/B,OAAO,WAAW,QAAQ,KAAA;GAC1B,WAAW,WAAW,KAAA,IAAY;GAClC,SACE,WAAW,KAAA,IACT,oBAAC,QAAD;IAAM,WAAWA,yBAAO;IAAS,eAAY;IAC3C,UAAA,oBAAC,iBAAD,EAAiB,MAAM,GAAK,CAAA;GACxB,CAAA;GAGV,WAAW,GAAG,YAAYC,0BAAiB,UAAU;GACrD,eAAgB,OAAO,UAAU,KAAK,IAAI,SAAS,OAAO;GAC1D,WAAW;GAEV,UAAA,WACC,oBAAC,QAAD;IAAM,WAAWA,0BAAiB;IAAM,eAAY;IACjD,UAAA;GACG,CAAA,IAEN;EAES,CAAA,GACb,oBAAC,UAAD;GACE,IAAI;GACG;GACD;GACO;GACJ;GACC;GACV,YAAY;GACL;GACI;GACD;GACV,SAAS;GACT,UAAU;EACX,CAAA,CACE;;AAET,CAAC"}
|
|
@@ -7,4 +7,6 @@ export declare function itemText(item: BlockMenuItem): string;
|
|
|
7
7
|
* forward from the item after `from` and wrapping around.
|
|
8
8
|
*/
|
|
9
9
|
export declare function typeaheadIndex(items: BlockMenuItem[], from: number, char: string): number;
|
|
10
|
+
/** Enabled item indexes among the menu items (separators excluded). */
|
|
11
|
+
export declare function enabledMenuIndexes(items: BlockMenuEntry[]): number[];
|
|
10
12
|
//# sourceMappingURL=BlockMenu.utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BlockMenu.utils.d.ts","sourceRoot":"","sources":["../../../../src/components/actions/BlockMenu/BlockMenu.utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvE,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,GAAG,KAAK,IAAI,aAAa,CAExE;AAED,8CAA8C;AAC9C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAGpD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzF"}
|
|
1
|
+
{"version":3,"file":"BlockMenu.utils.d.ts","sourceRoot":"","sources":["../../../../src/components/actions/BlockMenu/BlockMenu.utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvE,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,GAAG,KAAK,IAAI,aAAa,CAExE;AAED,8CAA8C;AAC9C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAGpD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzF;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,MAAM,EAAE,CAEpE"}
|
|
@@ -20,7 +20,11 @@ function typeaheadIndex(items, from, char) {
|
|
|
20
20
|
}
|
|
21
21
|
return -1;
|
|
22
22
|
}
|
|
23
|
+
/** Enabled item indexes among the menu items (separators excluded). */
|
|
24
|
+
function enabledMenuIndexes(items) {
|
|
25
|
+
return items.filter(isMenuItem).flatMap((item, index) => item.disabled ? [] : [index]);
|
|
26
|
+
}
|
|
23
27
|
//#endregion
|
|
24
|
-
export { isMenuItem, itemText, typeaheadIndex };
|
|
28
|
+
export { enabledMenuIndexes, isMenuItem, itemText, typeaheadIndex };
|
|
25
29
|
|
|
26
30
|
//# sourceMappingURL=BlockMenu.utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BlockMenu.utils.js","names":[],"sources":["../../../../src/components/actions/BlockMenu/BlockMenu.utils.ts"],"sourcesContent":["import type { BlockMenuEntry, BlockMenuItem } from \"./BlockMenu.types\";\n\nexport function isMenuItem(entry: BlockMenuEntry): entry is BlockMenuItem {\n return !(\"type\" in entry && entry.type === \"separator\");\n}\n\n/** Text an item answers to for type-ahead. */\nexport function itemText(item: BlockMenuItem): string {\n if (item.textValue !== undefined) return item.textValue;\n return typeof item.label === \"string\" || typeof item.label === \"number\" ? String(item.label) : \"\";\n}\n\n/**\n * Index of the next enabled item whose text starts with `char`, searching\n * forward from the item after `from` and wrapping around.\n */\nexport function typeaheadIndex(items: BlockMenuItem[], from: number, char: string): number {\n const needle = char.toLowerCase();\n for (let step = 1; step <= items.length; step += 1) {\n const index = (from + step) % items.length;\n const item = items[index];\n if (item && !item.disabled && itemText(item).toLowerCase().startsWith(needle)) return index;\n }\n return -1;\n}\n"],"mappings":";AAEA,SAAgB,WAAW,OAA+C;CACxE,OAAO,EAAE,UAAU,SAAS,MAAM,SAAS;AAC7C;;AAGA,SAAgB,SAAS,MAA6B;CACpD,IAAI,KAAK,cAAc,KAAA,GAAW,OAAO,KAAK;CAC9C,OAAO,OAAO,KAAK,UAAU,YAAY,OAAO,KAAK,UAAU,WAAW,OAAO,KAAK,KAAK,IAAI;AACjG;;;;;AAMA,SAAgB,eAAe,OAAwB,MAAc,MAAsB;CACzF,MAAM,SAAS,KAAK,YAAY;CAChC,KAAK,IAAI,OAAO,GAAG,QAAQ,MAAM,QAAQ,QAAQ,GAAG;EAClD,MAAM,SAAS,OAAO,QAAQ,MAAM;EACpC,MAAM,OAAO,MAAM;EACnB,IAAI,QAAQ,CAAC,KAAK,YAAY,SAAS,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,WAAW,MAAM,GAAG,OAAO;CACxF;CACA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"BlockMenu.utils.js","names":[],"sources":["../../../../src/components/actions/BlockMenu/BlockMenu.utils.ts"],"sourcesContent":["import type { BlockMenuEntry, BlockMenuItem } from \"./BlockMenu.types\";\n\nexport function isMenuItem(entry: BlockMenuEntry): entry is BlockMenuItem {\n return !(\"type\" in entry && entry.type === \"separator\");\n}\n\n/** Text an item answers to for type-ahead. */\nexport function itemText(item: BlockMenuItem): string {\n if (item.textValue !== undefined) return item.textValue;\n return typeof item.label === \"string\" || typeof item.label === \"number\" ? String(item.label) : \"\";\n}\n\n/**\n * Index of the next enabled item whose text starts with `char`, searching\n * forward from the item after `from` and wrapping around.\n */\nexport function typeaheadIndex(items: BlockMenuItem[], from: number, char: string): number {\n const needle = char.toLowerCase();\n for (let step = 1; step <= items.length; step += 1) {\n const index = (from + step) % items.length;\n const item = items[index];\n if (item && !item.disabled && itemText(item).toLowerCase().startsWith(needle)) return index;\n }\n return -1;\n}\n\n/** Enabled item indexes among the menu items (separators excluded). */\nexport function enabledMenuIndexes(items: BlockMenuEntry[]): number[] {\n return items.filter(isMenuItem).flatMap((item, index) => (item.disabled ? [] : [index]));\n}\n"],"mappings":";AAEA,SAAgB,WAAW,OAA+C;CACxE,OAAO,EAAE,UAAU,SAAS,MAAM,SAAS;AAC7C;;AAGA,SAAgB,SAAS,MAA6B;CACpD,IAAI,KAAK,cAAc,KAAA,GAAW,OAAO,KAAK;CAC9C,OAAO,OAAO,KAAK,UAAU,YAAY,OAAO,KAAK,UAAU,WAAW,OAAO,KAAK,KAAK,IAAI;AACjG;;;;;AAMA,SAAgB,eAAe,OAAwB,MAAc,MAAsB;CACzF,MAAM,SAAS,KAAK,YAAY;CAChC,KAAK,IAAI,OAAO,GAAG,QAAQ,MAAM,QAAQ,QAAQ,GAAG;EAClD,MAAM,SAAS,OAAO,QAAQ,MAAM;EACpC,MAAM,OAAO,MAAM;EACnB,IAAI,QAAQ,CAAC,KAAK,YAAY,SAAS,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,WAAW,MAAM,GAAG,OAAO;CACxF;CACA,OAAO;AACT;;AAGA,SAAgB,mBAAmB,OAAmC;CACpE,OAAO,MAAM,OAAO,UAAU,CAAC,CAAC,SAAS,MAAM,UAAW,KAAK,WAAW,CAAC,IAAI,CAAC,KAAK,CAAE;AACzF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
import type { AnchoredPosition } from "../../../hooks/useAnchoredPosition";
|
|
3
|
+
import type { BlockMenuEntry, BlockMenuItem } from "./BlockMenu.types";
|
|
4
|
+
export type MenuFocusTarget = "first" | "last" | number;
|
|
5
|
+
export interface MenuListProps {
|
|
6
|
+
id: string;
|
|
7
|
+
items: BlockMenuEntry[];
|
|
8
|
+
open: boolean;
|
|
9
|
+
/** Item to focus once the menu is open and positioned. */
|
|
10
|
+
focusTarget: MenuFocusTarget | null;
|
|
11
|
+
menuRef: RefObject<HTMLDivElement | null>;
|
|
12
|
+
position: AnchoredPosition | null;
|
|
13
|
+
labelledBy?: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
align?: "start" | "end";
|
|
16
|
+
placement?: string;
|
|
17
|
+
/** Whether a node belongs to the menu or its trigger (outside clicks / blur close the menu). */
|
|
18
|
+
isInside: (node: Node | null) => boolean;
|
|
19
|
+
onClose: (returnFocus: boolean) => void;
|
|
20
|
+
onChoose: (item: BlockMenuItem) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Internal `role="menu"` shared by `BlockMenu` and `ContextMenu`: rendered in the
|
|
24
|
+
* overlay layer, with roving focus, wrap-around arrows, Home / End, type-ahead,
|
|
25
|
+
* Escape / Tab to close and outside clicks closing it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function MenuList({ id, items, open, focusTarget, menuRef, position, labelledBy, label, align, placement, isInside, onClose, onChoose, }: MenuListProps): import("react").ReactPortal | null;
|
|
28
|
+
//# sourceMappingURL=MenuList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MenuList.d.ts","sourceRoot":"","sources":["../../../../src/components/actions/BlockMenu/MenuList.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA0D,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE/F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvE,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,0DAA0D;IAC1D,WAAW,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,OAAO,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC1C,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC;IACzC,OAAO,EAAE,CAAC,WAAW,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,QAAQ,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;CACzC;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EACvB,EAAE,EACF,KAAK,EACL,IAAI,EACJ,WAAW,EACX,OAAO,EACP,QAAQ,EACR,UAAU,EACV,KAAK,EACL,KAAK,EACL,SAAS,EACT,QAAQ,EACR,OAAO,EACP,QAAQ,GACT,EAAE,aAAa,sCA4If"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { usePortalContainer } from "../../../provider/context.js";
|
|
2
|
+
import BlockMenu_module_default from "./BlockMenu.module.js";
|
|
3
|
+
import { enabledMenuIndexes, isMenuItem, typeaheadIndex } from "./BlockMenu.utils.js";
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
import { createPortal } from "react-dom";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/components/actions/BlockMenu/MenuList.tsx
|
|
8
|
+
/**
|
|
9
|
+
* Internal `role="menu"` shared by `BlockMenu` and `ContextMenu`: rendered in the
|
|
10
|
+
* overlay layer, with roving focus, wrap-around arrows, Home / End, type-ahead,
|
|
11
|
+
* Escape / Tab to close and outside clicks closing it.
|
|
12
|
+
*/
|
|
13
|
+
function MenuList({ id, items, open, focusTarget, menuRef, position, labelledBy, label, align, placement, isInside, onClose, onChoose }) {
|
|
14
|
+
const container = usePortalContainer();
|
|
15
|
+
const itemRefs = useRef(/* @__PURE__ */ new Map());
|
|
16
|
+
const menuItems = items.filter(isMenuItem);
|
|
17
|
+
const enabledIndexes = enabledMenuIndexes(items);
|
|
18
|
+
const focusItem = (index) => {
|
|
19
|
+
const item = menuItems[index];
|
|
20
|
+
if (item) itemRefs.current.get(item.id)?.focus();
|
|
21
|
+
};
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!open || focusTarget === null || !position) return;
|
|
24
|
+
const index = focusTarget === "first" ? enabledIndexes[0] : focusTarget === "last" ? enabledIndexes[enabledIndexes.length - 1] : focusTarget;
|
|
25
|
+
if (index !== void 0) focusItem(index);
|
|
26
|
+
}, [
|
|
27
|
+
open,
|
|
28
|
+
focusTarget,
|
|
29
|
+
position !== null
|
|
30
|
+
]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!open) return;
|
|
33
|
+
const onPointerDown = (event) => {
|
|
34
|
+
if (!isInside(event.target)) onClose(false);
|
|
35
|
+
};
|
|
36
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
37
|
+
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
38
|
+
}, [open]);
|
|
39
|
+
const onKeyDown = (event) => {
|
|
40
|
+
const current = menuItems.findIndex((item) => itemRefs.current.get(item.id) === document.activeElement);
|
|
41
|
+
const at = enabledIndexes.indexOf(current);
|
|
42
|
+
const count = enabledIndexes.length;
|
|
43
|
+
let next;
|
|
44
|
+
switch (event.key) {
|
|
45
|
+
case "ArrowDown":
|
|
46
|
+
next = enabledIndexes[(at + 1) % count];
|
|
47
|
+
break;
|
|
48
|
+
case "ArrowUp":
|
|
49
|
+
next = enabledIndexes[(at - 1 + count) % count];
|
|
50
|
+
break;
|
|
51
|
+
case "Home":
|
|
52
|
+
next = enabledIndexes[0];
|
|
53
|
+
break;
|
|
54
|
+
case "End":
|
|
55
|
+
next = enabledIndexes[count - 1];
|
|
56
|
+
break;
|
|
57
|
+
case "Escape":
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
event.stopPropagation();
|
|
60
|
+
onClose(true);
|
|
61
|
+
return;
|
|
62
|
+
case "Tab":
|
|
63
|
+
onClose(true);
|
|
64
|
+
return;
|
|
65
|
+
default: if (event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey) {
|
|
66
|
+
const match = typeaheadIndex(menuItems, current, event.key);
|
|
67
|
+
if (match >= 0) next = match;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (next === void 0) return;
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
focusItem(next);
|
|
73
|
+
};
|
|
74
|
+
const onBlur = (event) => {
|
|
75
|
+
if (!isInside(event.relatedTarget)) onClose(false);
|
|
76
|
+
};
|
|
77
|
+
if (!container) return null;
|
|
78
|
+
return createPortal(/* @__PURE__ */ jsx("div", {
|
|
79
|
+
ref: menuRef,
|
|
80
|
+
id,
|
|
81
|
+
role: "menu",
|
|
82
|
+
tabIndex: -1,
|
|
83
|
+
"aria-labelledby": labelledBy,
|
|
84
|
+
"aria-label": labelledBy ? void 0 : label,
|
|
85
|
+
hidden: !open,
|
|
86
|
+
"data-align": align,
|
|
87
|
+
"data-placement": position?.side ?? placement,
|
|
88
|
+
className: BlockMenu_module_default.menu,
|
|
89
|
+
style: position?.style,
|
|
90
|
+
onKeyDown,
|
|
91
|
+
onBlur,
|
|
92
|
+
children: items.map((entry, index) => {
|
|
93
|
+
if (!isMenuItem(entry)) return /* @__PURE__ */ jsx("div", {
|
|
94
|
+
role: "separator",
|
|
95
|
+
className: BlockMenu_module_default.separator
|
|
96
|
+
}, entry.id ?? `separator-${index}`);
|
|
97
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
98
|
+
ref: (element) => {
|
|
99
|
+
if (element) itemRefs.current.set(entry.id, element);
|
|
100
|
+
else itemRefs.current.delete(entry.id);
|
|
101
|
+
},
|
|
102
|
+
type: "button",
|
|
103
|
+
role: "menuitem",
|
|
104
|
+
tabIndex: -1,
|
|
105
|
+
"aria-disabled": entry.disabled || void 0,
|
|
106
|
+
"data-danger": entry.danger || void 0,
|
|
107
|
+
className: BlockMenu_module_default.item,
|
|
108
|
+
onClick: () => {
|
|
109
|
+
if (!entry.disabled) onChoose(entry);
|
|
110
|
+
},
|
|
111
|
+
children: [
|
|
112
|
+
/* @__PURE__ */ jsx("span", {
|
|
113
|
+
className: BlockMenu_module_default.icon,
|
|
114
|
+
"aria-hidden": "true",
|
|
115
|
+
children: entry.icon
|
|
116
|
+
}),
|
|
117
|
+
/* @__PURE__ */ jsx("span", {
|
|
118
|
+
className: BlockMenu_module_default.label,
|
|
119
|
+
children: entry.label
|
|
120
|
+
}),
|
|
121
|
+
entry.shortcut ? /* @__PURE__ */ jsx("kbd", {
|
|
122
|
+
className: BlockMenu_module_default.shortcut,
|
|
123
|
+
"aria-hidden": "true",
|
|
124
|
+
children: entry.shortcut
|
|
125
|
+
}) : null
|
|
126
|
+
]
|
|
127
|
+
}, entry.id);
|
|
128
|
+
})
|
|
129
|
+
}), container);
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
export { MenuList };
|
|
133
|
+
|
|
134
|
+
//# sourceMappingURL=MenuList.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MenuList.js","names":["styles"],"sources":["../../../../src/components/actions/BlockMenu/MenuList.tsx"],"sourcesContent":["import { useEffect, useRef, type FocusEvent, type KeyboardEvent, type RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type { AnchoredPosition } from \"../../../hooks/useAnchoredPosition\";\nimport { usePortalContainer } from \"../../../provider/context\";\nimport styles from \"./BlockMenu.module.css\";\nimport type { BlockMenuEntry, BlockMenuItem } from \"./BlockMenu.types\";\nimport { enabledMenuIndexes, isMenuItem, typeaheadIndex } from \"./BlockMenu.utils\";\n\nexport type MenuFocusTarget = \"first\" | \"last\" | number;\n\nexport interface MenuListProps {\n id: string;\n items: BlockMenuEntry[];\n open: boolean;\n /** Item to focus once the menu is open and positioned. */\n focusTarget: MenuFocusTarget | null;\n menuRef: RefObject<HTMLDivElement | null>;\n position: AnchoredPosition | null;\n labelledBy?: string;\n label?: string;\n align?: \"start\" | \"end\";\n placement?: string;\n /** Whether a node belongs to the menu or its trigger (outside clicks / blur close the menu). */\n isInside: (node: Node | null) => boolean;\n onClose: (returnFocus: boolean) => void;\n onChoose: (item: BlockMenuItem) => void;\n}\n\n/**\n * Internal `role=\"menu\"` shared by `BlockMenu` and `ContextMenu`: rendered in the\n * overlay layer, with roving focus, wrap-around arrows, Home / End, type-ahead,\n * Escape / Tab to close and outside clicks closing it.\n */\nexport function MenuList({\n id,\n items,\n open,\n focusTarget,\n menuRef,\n position,\n labelledBy,\n label,\n align,\n placement,\n isInside,\n onClose,\n onChoose,\n}: MenuListProps) {\n const container = usePortalContainer();\n const itemRefs = useRef(new Map<string, HTMLButtonElement>());\n const menuItems = items.filter(isMenuItem);\n const enabledIndexes = enabledMenuIndexes(items);\n\n const focusItem = (index: number) => {\n const item = menuItems[index];\n if (item) itemRefs.current.get(item.id)?.focus();\n };\n\n useEffect(() => {\n if (!open || focusTarget === null || !position) return;\n const index =\n focusTarget === \"first\"\n ? enabledIndexes[0]\n : focusTarget === \"last\"\n ? enabledIndexes[enabledIndexes.length - 1]\n : focusTarget;\n if (index !== undefined) focusItem(index);\n // Focus once the menu is open and positioned, or when the target changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, focusTarget, position !== null]);\n\n useEffect(() => {\n if (!open) return;\n const onPointerDown = (event: PointerEvent) => {\n if (!isInside(event.target as Node)) onClose(false);\n };\n document.addEventListener(\"pointerdown\", onPointerDown);\n return () => document.removeEventListener(\"pointerdown\", onPointerDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n const current = menuItems.findIndex(\n (item) => itemRefs.current.get(item.id) === document.activeElement,\n );\n const at = enabledIndexes.indexOf(current);\n const count = enabledIndexes.length;\n let next: number | undefined;\n\n switch (event.key) {\n case \"ArrowDown\":\n next = enabledIndexes[(at + 1) % count];\n break;\n case \"ArrowUp\":\n next = enabledIndexes[(at - 1 + count) % count];\n break;\n case \"Home\":\n next = enabledIndexes[0];\n break;\n case \"End\":\n next = enabledIndexes[count - 1];\n break;\n case \"Escape\":\n event.preventDefault();\n event.stopPropagation();\n onClose(true);\n return;\n case \"Tab\":\n // The menu lives in the overlay layer at the end of the page: hand focus back to the\n // opener first so the browser's Tab continues from the opener's place in the page.\n onClose(true);\n return;\n default:\n if (event.key.length === 1 && /\\S/.test(event.key) && !event.ctrlKey && !event.metaKey) {\n const match = typeaheadIndex(menuItems, current, event.key);\n if (match >= 0) next = match;\n }\n }\n if (next === undefined) return;\n event.preventDefault();\n focusItem(next);\n };\n\n const onBlur = (event: FocusEvent<HTMLDivElement>) => {\n if (!isInside(event.relatedTarget as Node | null)) onClose(false);\n };\n\n if (!container) return null;\n\n return createPortal(\n <div\n ref={menuRef}\n id={id}\n role=\"menu\"\n tabIndex={-1}\n aria-labelledby={labelledBy}\n aria-label={labelledBy ? undefined : label}\n hidden={!open}\n data-align={align}\n data-placement={position?.side ?? placement}\n className={styles.menu}\n style={position?.style}\n onKeyDown={onKeyDown}\n onBlur={onBlur}\n >\n {items.map((entry, index) => {\n if (!isMenuItem(entry)) {\n return (\n <div\n key={entry.id ?? `separator-${index}`}\n role=\"separator\"\n className={styles.separator}\n />\n );\n }\n return (\n <button\n key={entry.id}\n ref={(element) => {\n if (element) itemRefs.current.set(entry.id, element);\n else itemRefs.current.delete(entry.id);\n }}\n type=\"button\"\n role=\"menuitem\"\n tabIndex={-1}\n aria-disabled={entry.disabled || undefined}\n data-danger={entry.danger || undefined}\n className={styles.item}\n onClick={() => {\n if (!entry.disabled) onChoose(entry);\n }}\n >\n <span className={styles.icon} aria-hidden=\"true\">\n {entry.icon}\n </span>\n <span className={styles.label}>{entry.label}</span>\n {entry.shortcut ? (\n <kbd className={styles.shortcut} aria-hidden=\"true\">\n {entry.shortcut}\n </kbd>\n ) : null}\n </button>\n );\n })}\n </div>,\n container,\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ContextMenuProps } from "./ContextMenu.types";
|
|
2
|
+
/**
|
|
3
|
+
* Right-click menu for an area (an inventory slot, a list row). Opens at the
|
|
4
|
+
* pointer, or at the focused element with Shift+F10 / the Menu key; keyboard
|
|
5
|
+
* behaviour matches `BlockMenu` and focus returns to where it was.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ContextMenu: import("react").ForwardRefExoticComponent<ContextMenuProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
8
|
+
//# sourceMappingURL=ContextMenu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContextMenu.d.ts","sourceRoot":"","sources":["../../../../src/components/actions/ContextMenu/ContextMenu.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAO5D;;;;GAIG;AACH,eAAO,MAAM,WAAW,6GA0GtB,CAAC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { cx } from "../../../utils/cx.js";
|
|
2
|
+
import { useAnchoredPosition } from "../../../hooks/useAnchoredPosition.js";
|
|
3
|
+
import { mergeRefs } from "../../../utils/refs.js";
|
|
4
|
+
import { enabledMenuIndexes } from "../BlockMenu/BlockMenu.utils.js";
|
|
5
|
+
import { MenuList } from "../BlockMenu/MenuList.js";
|
|
6
|
+
import ContextMenu_module_default from "./ContextMenu.module.js";
|
|
7
|
+
import { forwardRef, useId, useRef, useState } from "react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
//#region src/components/actions/ContextMenu/ContextMenu.tsx
|
|
10
|
+
/**
|
|
11
|
+
* Right-click menu for an area (an inventory slot, a list row). Opens at the
|
|
12
|
+
* pointer, or at the focused element with Shift+F10 / the Menu key; keyboard
|
|
13
|
+
* behaviour matches `BlockMenu` and focus returns to where it was.
|
|
14
|
+
*/
|
|
15
|
+
var ContextMenu = forwardRef(function ContextMenu({ children, items, onSelect, label = "Context menu", disabled = false, onOpenChange, className, ...rest }, ref) {
|
|
16
|
+
const menuId = useId();
|
|
17
|
+
const areaRef = useRef(null);
|
|
18
|
+
const menuRef = useRef(null);
|
|
19
|
+
const returnTo = useRef(null);
|
|
20
|
+
const point = useRef({
|
|
21
|
+
x: 0,
|
|
22
|
+
y: 0
|
|
23
|
+
});
|
|
24
|
+
const [open, setOpenState] = useState(false);
|
|
25
|
+
const [focusTarget, setFocusTarget] = useState(null);
|
|
26
|
+
const virtualAnchor = useRef({ getBoundingClientRect: () => DOMRect.fromRect({
|
|
27
|
+
x: point.current.x,
|
|
28
|
+
y: point.current.y,
|
|
29
|
+
width: 0,
|
|
30
|
+
height: 0
|
|
31
|
+
}) });
|
|
32
|
+
const position = useAnchoredPosition(virtualAnchor, menuRef, open, {
|
|
33
|
+
side: "bottom",
|
|
34
|
+
align: "start",
|
|
35
|
+
gap: 2
|
|
36
|
+
});
|
|
37
|
+
const setOpen = (next) => {
|
|
38
|
+
setOpenState(next);
|
|
39
|
+
onOpenChange?.(next);
|
|
40
|
+
};
|
|
41
|
+
const openAt = (x, y) => {
|
|
42
|
+
if (enabledMenuIndexes(items).length === 0) return;
|
|
43
|
+
returnTo.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
44
|
+
point.current = {
|
|
45
|
+
x,
|
|
46
|
+
y
|
|
47
|
+
};
|
|
48
|
+
setOpen(true);
|
|
49
|
+
setFocusTarget("first");
|
|
50
|
+
};
|
|
51
|
+
const close = (returnFocus) => {
|
|
52
|
+
setOpen(false);
|
|
53
|
+
setFocusTarget(null);
|
|
54
|
+
if (returnFocus) returnTo.current?.focus();
|
|
55
|
+
};
|
|
56
|
+
const choose = (item) => {
|
|
57
|
+
close(true);
|
|
58
|
+
item.onSelect?.();
|
|
59
|
+
onSelect?.(item.id);
|
|
60
|
+
};
|
|
61
|
+
const onContextMenu = (event) => {
|
|
62
|
+
if (disabled) return;
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
event.target.closest("button, a[href], input, [tabindex]")?.focus({ preventScroll: true });
|
|
65
|
+
openAt(event.clientX, event.clientY);
|
|
66
|
+
};
|
|
67
|
+
const onKeyDown = (event) => {
|
|
68
|
+
if (disabled) return;
|
|
69
|
+
if (event.key === "ContextMenu" || event.key === "F10" && event.shiftKey) {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
const rect = event.target.getBoundingClientRect();
|
|
72
|
+
openAt(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const isInside = (node) => Boolean(node && menuRef.current?.contains(node));
|
|
76
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
77
|
+
ref: mergeRefs(ref, areaRef),
|
|
78
|
+
className: cx(ContextMenu_module_default.area, className),
|
|
79
|
+
"data-open": open || void 0,
|
|
80
|
+
onContextMenu,
|
|
81
|
+
onKeyDown,
|
|
82
|
+
...rest,
|
|
83
|
+
children: [children, /* @__PURE__ */ jsx(MenuList, {
|
|
84
|
+
id: menuId,
|
|
85
|
+
items,
|
|
86
|
+
open,
|
|
87
|
+
focusTarget,
|
|
88
|
+
menuRef,
|
|
89
|
+
position,
|
|
90
|
+
label,
|
|
91
|
+
isInside,
|
|
92
|
+
onClose: close,
|
|
93
|
+
onChoose: choose
|
|
94
|
+
})]
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
//#endregion
|
|
98
|
+
export { ContextMenu };
|
|
99
|
+
|
|
100
|
+
//# sourceMappingURL=ContextMenu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContextMenu.js","names":["styles"],"sources":["../../../../src/components/actions/ContextMenu/ContextMenu.tsx"],"sourcesContent":["import { forwardRef, useId, useRef, useState, type KeyboardEvent, type MouseEvent } from \"react\";\nimport { useAnchoredPosition } from \"../../../hooks/useAnchoredPosition\";\nimport { cx } from \"../../../utils/cx\";\nimport { mergeRefs } from \"../../../utils/refs\";\nimport type { BlockMenuItem } from \"../BlockMenu/BlockMenu.types\";\nimport { enabledMenuIndexes } from \"../BlockMenu/BlockMenu.utils\";\nimport { MenuList, type MenuFocusTarget } from \"../BlockMenu/MenuList\";\nimport styles from \"./ContextMenu.module.css\";\nimport type { ContextMenuProps } from \"./ContextMenu.types\";\n\ninterface Point {\n x: number;\n y: number;\n}\n\n/**\n * Right-click menu for an area (an inventory slot, a list row). Opens at the\n * pointer, or at the focused element with Shift+F10 / the Menu key; keyboard\n * behaviour matches `BlockMenu` and focus returns to where it was.\n */\nexport const ContextMenu = forwardRef<HTMLDivElement, ContextMenuProps>(function ContextMenu(\n {\n children,\n items,\n onSelect,\n label = \"Context menu\",\n disabled = false,\n onOpenChange,\n className,\n ...rest\n },\n ref,\n) {\n const menuId = useId();\n const areaRef = useRef<HTMLDivElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n const returnTo = useRef<HTMLElement | null>(null);\n const point = useRef<Point>({ x: 0, y: 0 });\n const [open, setOpenState] = useState(false);\n const [focusTarget, setFocusTarget] = useState<MenuFocusTarget | null>(null);\n\n // A zero-size \"element\" at the pointer for the shared positioning hook.\n const virtualAnchor = useRef({\n getBoundingClientRect: () =>\n DOMRect.fromRect({ x: point.current.x, y: point.current.y, width: 0, height: 0 }),\n });\n const position = useAnchoredPosition(virtualAnchor, menuRef, open, {\n side: \"bottom\",\n align: \"start\",\n gap: 2,\n });\n\n const setOpen = (next: boolean) => {\n setOpenState(next);\n onOpenChange?.(next);\n };\n\n const openAt = (x: number, y: number) => {\n if (enabledMenuIndexes(items).length === 0) return;\n returnTo.current =\n document.activeElement instanceof HTMLElement ? document.activeElement : null;\n point.current = { x, y };\n setOpen(true);\n setFocusTarget(\"first\");\n };\n\n const close = (returnFocus: boolean) => {\n setOpen(false);\n setFocusTarget(null);\n if (returnFocus) returnTo.current?.focus();\n };\n\n const choose = (item: BlockMenuItem) => {\n close(true);\n item.onSelect?.();\n onSelect?.(item.id);\n };\n\n const onContextMenu = (event: MouseEvent<HTMLDivElement>) => {\n if (disabled) return;\n event.preventDefault();\n // Right-clicking does not move focus; remember the clicked control instead when it is focusable.\n const target = (event.target as HTMLElement).closest<HTMLElement>(\n \"button, a[href], input, [tabindex]\",\n );\n target?.focus({ preventScroll: true });\n openAt(event.clientX, event.clientY);\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (disabled) return;\n if (event.key === \"ContextMenu\" || (event.key === \"F10\" && event.shiftKey)) {\n event.preventDefault();\n const rect = (event.target as HTMLElement).getBoundingClientRect();\n openAt(rect.left + rect.width / 2, rect.top + rect.height / 2);\n }\n };\n\n const isInside = (node: Node | null) => Boolean(node && menuRef.current?.contains(node));\n\n return (\n // The area only listens for right-clicks and the Menu key bubbling up from the controls inside it.\n // eslint-disable-next-line jsx-a11y/no-static-element-interactions\n <div\n ref={mergeRefs(ref, areaRef)}\n className={cx(styles.area, className)}\n data-open={open || undefined}\n onContextMenu={onContextMenu}\n onKeyDown={onKeyDown}\n {...rest}\n >\n {children}\n <MenuList\n id={menuId}\n items={items}\n open={open}\n focusTarget={focusTarget}\n menuRef={menuRef}\n position={position}\n label={label}\n isInside={isInside}\n onClose={close}\n onChoose={choose}\n />\n </div>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;AAoBA,IAAa,cAAc,WAA6C,SAAS,YAC/E,EACE,UACA,OACA,UACA,QAAQ,gBACR,WAAW,OACX,cACA,WACA,GAAG,QAEL,KACA;CACA,MAAM,SAAS,MAAM;CACrB,MAAM,UAAU,OAAuB,IAAI;CAC3C,MAAM,UAAU,OAAuB,IAAI;CAC3C,MAAM,WAAW,OAA2B,IAAI;CAChD,MAAM,QAAQ,OAAc;EAAE,GAAG;EAAG,GAAG;CAAE,CAAC;CAC1C,MAAM,CAAC,MAAM,gBAAgB,SAAS,KAAK;CAC3C,MAAM,CAAC,aAAa,kBAAkB,SAAiC,IAAI;CAG3E,MAAM,gBAAgB,OAAO,EAC3B,6BACE,QAAQ,SAAS;EAAE,GAAG,MAAM,QAAQ;EAAG,GAAG,MAAM,QAAQ;EAAG,OAAO;EAAG,QAAQ;CAAE,CAAC,EACpF,CAAC;CACD,MAAM,WAAW,oBAAoB,eAAe,SAAS,MAAM;EACjE,MAAM;EACN,OAAO;EACP,KAAK;CACP,CAAC;CAED,MAAM,WAAW,SAAkB;EACjC,aAAa,IAAI;EACjB,eAAe,IAAI;CACrB;CAEA,MAAM,UAAU,GAAW,MAAc;EACvC,IAAI,mBAAmB,KAAK,CAAC,CAAC,WAAW,GAAG;EAC5C,SAAS,UACP,SAAS,yBAAyB,cAAc,SAAS,gBAAgB;EAC3E,MAAM,UAAU;GAAE;GAAG;EAAE;EACvB,QAAQ,IAAI;EACZ,eAAe,OAAO;CACxB;CAEA,MAAM,SAAS,gBAAyB;EACtC,QAAQ,KAAK;EACb,eAAe,IAAI;EACnB,IAAI,aAAa,SAAS,SAAS,MAAM;CAC3C;CAEA,MAAM,UAAU,SAAwB;EACtC,MAAM,IAAI;EACV,KAAK,WAAW;EAChB,WAAW,KAAK,EAAE;CACpB;CAEA,MAAM,iBAAiB,UAAsC;EAC3D,IAAI,UAAU;EACd,MAAM,eAAe;EAKrB,MAHsB,OAAuB,QAC3C,oCAEF,CAAA,EAAQ,MAAM,EAAE,eAAe,KAAK,CAAC;EACrC,OAAO,MAAM,SAAS,MAAM,OAAO;CACrC;CAEA,MAAM,aAAa,UAAyC;EAC1D,IAAI,UAAU;EACd,IAAI,MAAM,QAAQ,iBAAkB,MAAM,QAAQ,SAAS,MAAM,UAAW;GAC1E,MAAM,eAAe;GACrB,MAAM,OAAQ,MAAM,OAAuB,sBAAsB;GACjE,OAAO,KAAK,OAAO,KAAK,QAAQ,GAAG,KAAK,MAAM,KAAK,SAAS,CAAC;EAC/D;CACF;CAEA,MAAM,YAAY,SAAsB,QAAQ,QAAQ,QAAQ,SAAS,SAAS,IAAI,CAAC;CAEvF,OAGE,qBAAC,OAAD;EACE,KAAK,UAAU,KAAK,OAAO;EAC3B,WAAW,GAAGA,2BAAO,MAAM,SAAS;EACpC,aAAW,QAAQ,KAAA;EACJ;EACJ;EACX,GAAI;EANN,UAAA,CAQG,UACD,oBAAC,UAAD;GACE,IAAI;GACG;GACD;GACO;GACJ;GACC;GACH;GACG;GACV,SAAS;GACT,UAAU;EACX,CAAA,CACE;;AAET,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/components/actions/ContextMenu/ContextMenu.module.css
|
|
2
|
+
var area = "block-area-Zq0O2";
|
|
3
|
+
var ContextMenu_module_default = { area };
|
|
4
|
+
//#endregion
|
|
5
|
+
export { area, ContextMenu_module_default as default };
|
|
6
|
+
|
|
7
|
+
//# sourceMappingURL=ContextMenu.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContextMenu.module.js","names":[],"sources":["../../../../src/components/actions/ContextMenu/ContextMenu.module.css"],"sourcesContent":["/* The area takes the size of its content; it adds no visual of its own. */\n.area {\n display: contents;\n}\n"],"mappings":""}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import type { BlockMenuEntry } from "../BlockMenu/BlockMenu.types";
|
|
3
|
+
export interface ContextMenuProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
4
|
+
/** Area that opens the menu on right-click, Shift+F10 or the Menu key. */
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
items: BlockMenuEntry[];
|
|
7
|
+
/** Called with the item id after any item is chosen. */
|
|
8
|
+
onSelect?: (id: string) => void;
|
|
9
|
+
/** Accessible name of the menu. @default "Context menu" */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Keep the browser's own menu. */
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
onOpenChange?: (open: boolean) => void;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=ContextMenu.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContextMenu.types.d.ts","sourceRoot":"","sources":["../../../../src/components/actions/ContextMenu/ContextMenu.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC;IACxF,0EAA0E;IAC1E,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/actions/ContextMenu/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
|