@dmsi/wedgekit-react 0.0.16 → 0.0.18
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/NestedMenu.cjs +603 -0
- package/dist/components/NestedMenu.js +128 -0
- package/package.json +1 -1
- package/src/components/NestedMenu.tsx +100 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useKeydown.ts +42 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __defProps = Object.defineProperties;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
8
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
9
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
10
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
13
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
14
|
+
var __spreadValues = (a, b) => {
|
|
15
|
+
for (var prop in b || (b = {}))
|
|
16
|
+
if (__hasOwnProp.call(b, prop))
|
|
17
|
+
__defNormalProp(a, prop, b[prop]);
|
|
18
|
+
if (__getOwnPropSymbols)
|
|
19
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
20
|
+
if (__propIsEnum.call(b, prop))
|
|
21
|
+
__defNormalProp(a, prop, b[prop]);
|
|
22
|
+
}
|
|
23
|
+
return a;
|
|
24
|
+
};
|
|
25
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
26
|
+
var __objRest = (source, exclude) => {
|
|
27
|
+
var target = {};
|
|
28
|
+
for (var prop in source)
|
|
29
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
30
|
+
target[prop] = source[prop];
|
|
31
|
+
if (source != null && __getOwnPropSymbols)
|
|
32
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
33
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
34
|
+
target[prop] = source[prop];
|
|
35
|
+
}
|
|
36
|
+
return target;
|
|
37
|
+
};
|
|
38
|
+
var __export = (target, all) => {
|
|
39
|
+
for (var name in all)
|
|
40
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
41
|
+
};
|
|
42
|
+
var __copyProps = (to, from, except, desc) => {
|
|
43
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
44
|
+
for (let key of __getOwnPropNames(from))
|
|
45
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
46
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
47
|
+
}
|
|
48
|
+
return to;
|
|
49
|
+
};
|
|
50
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
51
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
52
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
53
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
54
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
55
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
56
|
+
mod
|
|
57
|
+
));
|
|
58
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
59
|
+
|
|
60
|
+
// src/components/NestedMenu.tsx
|
|
61
|
+
var NestedMenu_exports = {};
|
|
62
|
+
__export(NestedMenu_exports, {
|
|
63
|
+
NestedMenu: () => NestedMenu
|
|
64
|
+
});
|
|
65
|
+
module.exports = __toCommonJS(NestedMenu_exports);
|
|
66
|
+
var import_react4 = require("react");
|
|
67
|
+
|
|
68
|
+
// src/components/Icon.tsx
|
|
69
|
+
var import_clsx = __toESM(require("clsx"), 1);
|
|
70
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
71
|
+
function Icon(_a) {
|
|
72
|
+
var _b = _a, {
|
|
73
|
+
name,
|
|
74
|
+
size = 24,
|
|
75
|
+
variant = "outline"
|
|
76
|
+
} = _b, props = __objRest(_b, [
|
|
77
|
+
"name",
|
|
78
|
+
"size",
|
|
79
|
+
"variant"
|
|
80
|
+
]);
|
|
81
|
+
const variantStyle = variant === "filled" ? '"FILL" 1' : '"FILL" 0';
|
|
82
|
+
const weightStyle = size === 16 ? '"wght" 400' : '"wght" 300';
|
|
83
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
84
|
+
"span",
|
|
85
|
+
__spreadProps(__spreadValues({}, props), {
|
|
86
|
+
className: (0, import_clsx.default)(
|
|
87
|
+
"icon",
|
|
88
|
+
`icon-${size}`,
|
|
89
|
+
// size === 16 ? "font-normal" : "font-light", // size 16 font weight is not working as normal from before
|
|
90
|
+
props.className
|
|
91
|
+
),
|
|
92
|
+
style: __spreadValues({
|
|
93
|
+
fontVariationSettings: variantStyle + `, ${weightStyle}`
|
|
94
|
+
}, props.style),
|
|
95
|
+
children: name
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/components/MenuOption.tsx
|
|
101
|
+
var import_clsx5 = __toESM(require("clsx"), 1);
|
|
102
|
+
var import_react2 = require("react");
|
|
103
|
+
|
|
104
|
+
// src/classNames.ts
|
|
105
|
+
var import_clsx2 = __toESM(require("clsx"), 1);
|
|
106
|
+
var typography = {
|
|
107
|
+
display1: (0, import_clsx2.default)(
|
|
108
|
+
"font-sans font-semibold",
|
|
109
|
+
"text-display-1-mobile desktop:text-display-1-desktop compact:text-display-1-desktop-compact",
|
|
110
|
+
"leading-display-1-mobile desktop:leading-display-1-desktop"
|
|
111
|
+
),
|
|
112
|
+
display2: (0, import_clsx2.default)(
|
|
113
|
+
"font-sans font-bold",
|
|
114
|
+
"text-display-2-mobile desktop:text-display-2-desktop compact:text-display-2-desktop-compact",
|
|
115
|
+
"leading-display-2-mobile desktop:leading-display-2-desktop"
|
|
116
|
+
),
|
|
117
|
+
heading1: (0, import_clsx2.default)(
|
|
118
|
+
"font-sans font-semibold",
|
|
119
|
+
"text-heading-1-mobile desktop:text-heading-1-desktop compact:text-heading-1-desktop-compact",
|
|
120
|
+
"leading-heading-1-mobile desktop:leading-heading-1-desktop"
|
|
121
|
+
),
|
|
122
|
+
heading2: (0, import_clsx2.default)(
|
|
123
|
+
"font-sans font-normal",
|
|
124
|
+
"text-heading-2-mobile desktop:text-heading-2-desktop compact:text-heading-2-desktop-compact",
|
|
125
|
+
"leading-heading-2-mobile desktop:leading-heading-2-desktop"
|
|
126
|
+
),
|
|
127
|
+
heading3: (0, import_clsx2.default)(
|
|
128
|
+
"font-sans font-semibold",
|
|
129
|
+
"text-heading-3-mobile desktop:text-heading-3-desktop compact:text-heading-3-desktop-compact",
|
|
130
|
+
"leading-heading-3-mobile desktop:leading-heading-3-desktop"
|
|
131
|
+
),
|
|
132
|
+
subheader: (0, import_clsx2.default)(
|
|
133
|
+
"font-sans font-semibold",
|
|
134
|
+
"text-subheader-mobile desktop:text-subheader-desktop compact:text-subheader-desktop-compact",
|
|
135
|
+
"leading-subheader-mobile desktop:leading-subheader-desktop"
|
|
136
|
+
),
|
|
137
|
+
link: (0, import_clsx2.default)(
|
|
138
|
+
"font-sans font-normal",
|
|
139
|
+
"text-link-mobile desktop:text-link-desktop compact:text-link-desktop-compact",
|
|
140
|
+
"leading-link-mobile desktop:leading-link-desktop"
|
|
141
|
+
),
|
|
142
|
+
buttonLabel: (0, import_clsx2.default)(
|
|
143
|
+
"font-sans font-semibold",
|
|
144
|
+
"text-label-mobile desktop:text-label-desktop compact:text-label-desktop-compact",
|
|
145
|
+
"leading-label-mobile desktop:leading-label-desktop"
|
|
146
|
+
),
|
|
147
|
+
label: (0, import_clsx2.default)(
|
|
148
|
+
"font-sans font-semibold",
|
|
149
|
+
"text-label-mobile desktop:text-label-desktop compact:text-label-desktop-compact",
|
|
150
|
+
"leading-label-mobile desktop:leading-label-desktop"
|
|
151
|
+
),
|
|
152
|
+
paragraph: (0, import_clsx2.default)(
|
|
153
|
+
"font-sans font-normal",
|
|
154
|
+
"text-paragraph-mobile desktop:text-paragraph-desktop compact:text-paragraph-desktop-compact",
|
|
155
|
+
"leading-paragraph-mobile desktop:leading-paragraph-desktop"
|
|
156
|
+
),
|
|
157
|
+
caption: (0, import_clsx2.default)(
|
|
158
|
+
"font-sans font-normal",
|
|
159
|
+
"text-caption-mobile desktop:text-caption-desktop compact:text-caption-desktop-compact",
|
|
160
|
+
"leading-caption-mobile desktop:leading-caption-desktop"
|
|
161
|
+
)
|
|
162
|
+
};
|
|
163
|
+
var baseTransition = (0, import_clsx2.default)(
|
|
164
|
+
"transition-colors duration-100 ease-in-out"
|
|
165
|
+
);
|
|
166
|
+
var componentGap = (0, import_clsx2.default)(
|
|
167
|
+
"gap-mobile-component-gap desktop:gap-desktop-component-gap compact:gap-desktop-compact-component-gap"
|
|
168
|
+
);
|
|
169
|
+
var paddingUsingComponentGap = (0, import_clsx2.default)(
|
|
170
|
+
"p-mobile-component-gap desktop:p-desktop-component-gap compact:p-desktop-compact-component-gap"
|
|
171
|
+
);
|
|
172
|
+
var paddingXUsingLayoutGroupGap = (0, import_clsx2.default)(
|
|
173
|
+
"px-mobile-layout-group-gap desktop:px-desktop-layout-group-gap compact:px-desktop-compact-layout-group-gap"
|
|
174
|
+
);
|
|
175
|
+
var paddingYUsingLayoutGroupGap = (0, import_clsx2.default)(
|
|
176
|
+
"py-mobile-layout-group-gap desktop:py-desktop-layout-group-gap compact:py-desktop-compact-layout-group-gap"
|
|
177
|
+
);
|
|
178
|
+
var componentPadding = (0, import_clsx2.default)(
|
|
179
|
+
"p-mobile-component-padding desktop:p-desktop-component-padding compact:p-desktop-compact-component-padding"
|
|
180
|
+
);
|
|
181
|
+
var componentPaddingBottom = (0, import_clsx2.default)(
|
|
182
|
+
"pb-mobile-component-padding desktop:pb-desktop-component-padding compact:pb-desktop-compact-component-padding"
|
|
183
|
+
);
|
|
184
|
+
var componentPaddingX = (0, import_clsx2.default)(
|
|
185
|
+
"px-mobile-component-padding desktop:px-desktop-component-padding compact:px-desktop-compact-component-padding"
|
|
186
|
+
);
|
|
187
|
+
var componentPaddingY = (0, import_clsx2.default)(
|
|
188
|
+
"py-mobile-component-padding desktop:py-desktop-component-padding compact:py-desktop-compact-component-padding"
|
|
189
|
+
);
|
|
190
|
+
var componentPaddingXUsingComponentGap = (0, import_clsx2.default)(
|
|
191
|
+
"px-mobile-component-gap desktop:px-desktop-component-gap compact:px-desktop-compact-component-gap"
|
|
192
|
+
);
|
|
193
|
+
var componentPaddingYUsingComponentGap = (0, import_clsx2.default)(
|
|
194
|
+
"py-mobile-component-gap desktop:py-desktop-component-gap compact:py-desktop-compact-component-gap"
|
|
195
|
+
);
|
|
196
|
+
var componentPaddingMinusBorder = (0, import_clsx2.default)(
|
|
197
|
+
"p-[calc(var(--spacing-mobile-component-padding)_-_1px)] desktop:p-[calc(var(--spacing-desktop-component-padding)_-_1px)] compact:p-[calc(var(--spacing-desktop-compact-component-padding)_-_1px)]"
|
|
198
|
+
);
|
|
199
|
+
var componentPaddingMinus2pxBorder = (0, import_clsx2.default)(
|
|
200
|
+
"p-[calc(var(--spacing-mobile-component-padding)_-_2px)] desktop:p-[calc(var(--spacing-desktop-component-padding)_-_2px)] compact:p-[calc(var(--spacing-desktop-compact-component-padding)_-_2px)]"
|
|
201
|
+
);
|
|
202
|
+
var layoutPaddding = (0, import_clsx2.default)(
|
|
203
|
+
"p-mobile-layout-padding desktop:p-desktop-layout-padding compact:p-desktop-compact-layout-padding"
|
|
204
|
+
);
|
|
205
|
+
var layoutPaddingBottom = (0, import_clsx2.default)(
|
|
206
|
+
"pb-mobile-layout-padding desktop:pb-desktop-layout-padding compact:pb-desktop-compact-layout-padding"
|
|
207
|
+
);
|
|
208
|
+
var layoutPaddingY = (0, import_clsx2.default)(
|
|
209
|
+
"py-mobile-layout-padding desktop:py-desktop-layout-padding compact:py-desktop-compact-layout-padding"
|
|
210
|
+
);
|
|
211
|
+
var containerPaddingX = (0, import_clsx2.default)(
|
|
212
|
+
"px-mobile-container-padding desktop:px-desktop-container-padding compact:px-desktop-compact-container-padding"
|
|
213
|
+
);
|
|
214
|
+
var containerPaddingY = (0, import_clsx2.default)(
|
|
215
|
+
"py-mobile-container-padding desktop:py-desktop-container-padding compact:py-desktop-compact-container-padding"
|
|
216
|
+
);
|
|
217
|
+
var layoutGroupGapPaddingY = (0, import_clsx2.default)(
|
|
218
|
+
"py-mobile-layout-group-gap desktop:py-desktop-layout-group-gap compact:py-desktop-compact-layout-group-gap"
|
|
219
|
+
);
|
|
220
|
+
var layoutGroupGap = (0, import_clsx2.default)(
|
|
221
|
+
"gap-mobile-layout-group-gap desktop:gap-desktop-layout-group-gap compact:gap-desktop-compact-layout-group-gap"
|
|
222
|
+
);
|
|
223
|
+
var layoutGap = (0, import_clsx2.default)(
|
|
224
|
+
"gap-mobile-layout-gap desktop:gap-desktop-layout-gap compact:gap-desktop-compact-layout-gap"
|
|
225
|
+
);
|
|
226
|
+
var gapUsingContainerPadding = (0, import_clsx2.default)(
|
|
227
|
+
"gap-mobile-container-padding desktop:gap-desktop-container-padding compact:gap-desktop-compact-container-padding"
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// src/components/Label.tsx
|
|
231
|
+
var import_clsx3 = __toESM(require("clsx"), 1);
|
|
232
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
233
|
+
var Label = (_a) => {
|
|
234
|
+
var _b = _a, {
|
|
235
|
+
as = "span",
|
|
236
|
+
padded,
|
|
237
|
+
className,
|
|
238
|
+
color,
|
|
239
|
+
align
|
|
240
|
+
} = _b, props = __objRest(_b, [
|
|
241
|
+
"as",
|
|
242
|
+
"padded",
|
|
243
|
+
"className",
|
|
244
|
+
"color",
|
|
245
|
+
"align"
|
|
246
|
+
]);
|
|
247
|
+
const Element = as;
|
|
248
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
249
|
+
Element,
|
|
250
|
+
__spreadProps(__spreadValues({
|
|
251
|
+
className: (0, import_clsx3.default)(
|
|
252
|
+
typography.label,
|
|
253
|
+
align === "left" && "text-left",
|
|
254
|
+
align === "center" && "text-center",
|
|
255
|
+
align === "right" && "text-right",
|
|
256
|
+
className,
|
|
257
|
+
padded && componentPaddingXUsingComponentGap
|
|
258
|
+
)
|
|
259
|
+
}, props), {
|
|
260
|
+
style: __spreadProps(__spreadValues({}, props.style), {
|
|
261
|
+
color: color ? `var(--color-${color})` : void 0
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
Label.displayName = "Label";
|
|
267
|
+
|
|
268
|
+
// src/components/Paragraph.tsx
|
|
269
|
+
var import_clsx4 = __toESM(require("clsx"), 1);
|
|
270
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
271
|
+
var Paragraph = (_a) => {
|
|
272
|
+
var _b = _a, {
|
|
273
|
+
className,
|
|
274
|
+
color,
|
|
275
|
+
padded,
|
|
276
|
+
align = "left",
|
|
277
|
+
tall,
|
|
278
|
+
addOverflow,
|
|
279
|
+
children,
|
|
280
|
+
as = "p"
|
|
281
|
+
} = _b, props = __objRest(_b, [
|
|
282
|
+
"className",
|
|
283
|
+
"color",
|
|
284
|
+
"padded",
|
|
285
|
+
"align",
|
|
286
|
+
"tall",
|
|
287
|
+
"addOverflow",
|
|
288
|
+
"children",
|
|
289
|
+
"as"
|
|
290
|
+
]);
|
|
291
|
+
const Element = as;
|
|
292
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
293
|
+
Element,
|
|
294
|
+
__spreadProps(__spreadValues({}, props), {
|
|
295
|
+
className: (0, import_clsx4.default)(
|
|
296
|
+
typography.paragraph,
|
|
297
|
+
className,
|
|
298
|
+
padded && componentPaddingXUsingComponentGap,
|
|
299
|
+
align === "left" && "text-left",
|
|
300
|
+
align === "center" && "text-center",
|
|
301
|
+
align === "right" && "text-right",
|
|
302
|
+
tall && "!leading-6",
|
|
303
|
+
addOverflow && "whitespace-nowrap text-ellipsis overflow-hidden"
|
|
304
|
+
),
|
|
305
|
+
style: __spreadProps(__spreadValues({}, props.style), {
|
|
306
|
+
color: color ? `var(--color-${color})` : void 0
|
|
307
|
+
}),
|
|
308
|
+
children
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
};
|
|
312
|
+
Paragraph.displayName = "Paragraph";
|
|
313
|
+
|
|
314
|
+
// src/components/useMatchesMedia.tsx
|
|
315
|
+
var import_react = require("react");
|
|
316
|
+
var useMatchesMedia = (query) => {
|
|
317
|
+
const [matches, setMatches] = (0, import_react.useState)(
|
|
318
|
+
() => typeof window !== "undefined" ? window.matchMedia(query).matches : false
|
|
319
|
+
);
|
|
320
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
321
|
+
const mediaQueryList = window.matchMedia(query);
|
|
322
|
+
const listener = (event) => {
|
|
323
|
+
setMatches(event.matches);
|
|
324
|
+
};
|
|
325
|
+
mediaQueryList.addEventListener("change", listener);
|
|
326
|
+
setMatches(mediaQueryList.matches);
|
|
327
|
+
return () => {
|
|
328
|
+
mediaQueryList.removeEventListener("change", listener);
|
|
329
|
+
};
|
|
330
|
+
}, [query]);
|
|
331
|
+
return matches;
|
|
332
|
+
};
|
|
333
|
+
var useMatchesMobile = () => {
|
|
334
|
+
const isMobile = useMatchesMedia("(width < 48rem)");
|
|
335
|
+
return isMobile;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// src/components/MenuOption.tsx
|
|
339
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
340
|
+
var MenuOption = ({
|
|
341
|
+
children,
|
|
342
|
+
disabled = false,
|
|
343
|
+
variant = "normal",
|
|
344
|
+
value,
|
|
345
|
+
before,
|
|
346
|
+
after,
|
|
347
|
+
subMenu,
|
|
348
|
+
onClick,
|
|
349
|
+
selected,
|
|
350
|
+
ref,
|
|
351
|
+
onSubMenuHover,
|
|
352
|
+
onSubMenuLeave,
|
|
353
|
+
onSubMenuEnter,
|
|
354
|
+
toggleMenu,
|
|
355
|
+
subMenuLevel = 1,
|
|
356
|
+
currentSubMenuLevel,
|
|
357
|
+
closeSubMenuLevel,
|
|
358
|
+
activeMenu,
|
|
359
|
+
mobilePositionTo,
|
|
360
|
+
highlightMatchingText = false,
|
|
361
|
+
menuValue
|
|
362
|
+
}) => {
|
|
363
|
+
const uniqueId = (0, import_react2.useId)();
|
|
364
|
+
const internalRef = (0, import_react2.useRef)(null);
|
|
365
|
+
const actualRef = ref || internalRef;
|
|
366
|
+
const menuId = (0, import_react2.useRef)(`menu-${uniqueId}`);
|
|
367
|
+
const isMobile = useMatchesMobile();
|
|
368
|
+
const handleMouseEnter = () => {
|
|
369
|
+
if (subMenu && onSubMenuHover && !disabled) {
|
|
370
|
+
onSubMenuHover(menuId.current, subMenuLevel);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const handleMouseLeave = () => {
|
|
374
|
+
if (subMenu && onSubMenuLeave && !disabled) {
|
|
375
|
+
onSubMenuLeave(subMenuLevel);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const handleSubMenuEnter = () => {
|
|
379
|
+
if (onSubMenuEnter) {
|
|
380
|
+
onSubMenuEnter();
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
const additionalAttributes = {
|
|
384
|
+
"data-selected": selected || null
|
|
385
|
+
};
|
|
386
|
+
const svgStyles = (0, import_clsx5.default)(
|
|
387
|
+
"[&>svg]:shrink-0 [&>svg]:fill-icon-action-primary-normal"
|
|
388
|
+
);
|
|
389
|
+
const textLabelStyles = (0, import_clsx5.default)("w-full whitespace-nowrap !leading-6");
|
|
390
|
+
const normalStyles = variant === "normal" && !disabled && (0, import_clsx5.default)(
|
|
391
|
+
"bg-transparent text-text-primary-normal",
|
|
392
|
+
"hover:bg-background-action-secondary-hover",
|
|
393
|
+
"focus:bg-background-action-secondary-hover",
|
|
394
|
+
"data-selected:bg-background-action-secondary-hover",
|
|
395
|
+
"active:bg-background-action-secondary-active"
|
|
396
|
+
);
|
|
397
|
+
const normalDisabledStyles = variant === "normal" && disabled && (0, import_clsx5.default)("text-text-primary-disabled");
|
|
398
|
+
const actionStyles = variant === "action" && !disabled && (0, import_clsx5.default)(
|
|
399
|
+
"text-action-400 bg-transparent",
|
|
400
|
+
"hover:bg-background-action-secondary-hover hover:text-text-action-hover",
|
|
401
|
+
"focus:bg-background-action-secondary-hover focus:text-text-action-hover",
|
|
402
|
+
"data-selected:bg-background-action-secondary-active data-selected:text-text-action-active",
|
|
403
|
+
"active:bg-background-action-secondary-active active:text-text-action-active"
|
|
404
|
+
);
|
|
405
|
+
const actionDisabledStyles = variant === "action" && disabled && (0, import_clsx5.default)("text-text-action-disabled");
|
|
406
|
+
const disabledStyles = disabled && (0, import_clsx5.default)("bg-transparent cursor-default pointer-events-none");
|
|
407
|
+
const renderChildren = typeof children === "string" && highlightMatchingText ? highlightMatch(children, menuValue) : children;
|
|
408
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
409
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
410
|
+
"div",
|
|
411
|
+
__spreadProps(__spreadValues({
|
|
412
|
+
ref: actualRef,
|
|
413
|
+
className: (0, import_clsx5.default)(
|
|
414
|
+
"flex items-center cursor-pointer w-full text-left relative outline-none",
|
|
415
|
+
svgStyles,
|
|
416
|
+
componentGap,
|
|
417
|
+
componentPadding,
|
|
418
|
+
baseTransition,
|
|
419
|
+
normalStyles,
|
|
420
|
+
normalDisabledStyles,
|
|
421
|
+
actionStyles,
|
|
422
|
+
actionDisabledStyles,
|
|
423
|
+
disabledStyles
|
|
424
|
+
),
|
|
425
|
+
"data-value": value || children,
|
|
426
|
+
onClick: () => {
|
|
427
|
+
onClick == null ? void 0 : onClick(menuId.current, value || children);
|
|
428
|
+
if (subMenu) {
|
|
429
|
+
toggleMenu(menuId.current, subMenuLevel);
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
onMouseEnter: handleMouseEnter,
|
|
433
|
+
onMouseLeave: handleMouseLeave
|
|
434
|
+
}, additionalAttributes), {
|
|
435
|
+
tabIndex: -1,
|
|
436
|
+
role: "menuitem",
|
|
437
|
+
"aria-haspopup": subMenu ? "menu" : void 0,
|
|
438
|
+
children: [
|
|
439
|
+
before && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "shrink-0 flex items-center", children: before }),
|
|
440
|
+
variant === "action" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Label, { padded: true, className: textLabelStyles, children: renderChildren }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Paragraph, { padded: true, className: textLabelStyles, children: renderChildren }),
|
|
441
|
+
renderAfterProp()
|
|
442
|
+
]
|
|
443
|
+
})
|
|
444
|
+
),
|
|
445
|
+
subMenu && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
446
|
+
"div",
|
|
447
|
+
{
|
|
448
|
+
onMouseEnter: handleSubMenuEnter,
|
|
449
|
+
onMouseLeave: handleMouseLeave,
|
|
450
|
+
"data-submenu-parent": menuId.current,
|
|
451
|
+
"data-menu-level": subMenuLevel + 1,
|
|
452
|
+
children: subMenu({ menuId: menuId.current, positionTo: actualRef, mobilePositionTo, position: "right", subMenuLevel, mobileBackMenuOption, mobileHide: isMobile && activeMenu !== menuId.current })
|
|
453
|
+
}
|
|
454
|
+
)
|
|
455
|
+
] });
|
|
456
|
+
function renderAfterProp() {
|
|
457
|
+
if (after) {
|
|
458
|
+
return after;
|
|
459
|
+
}
|
|
460
|
+
if (subMenu && after !== null) {
|
|
461
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Icon, { name: "chevron_right" });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function mobileBackMenuOption() {
|
|
465
|
+
if (!isMobile) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
469
|
+
MenuOption,
|
|
470
|
+
{
|
|
471
|
+
onClick: () => {
|
|
472
|
+
closeSubMenuLevel == null ? void 0 : closeSubMenuLevel(currentSubMenuLevel != null ? currentSubMenuLevel : 0);
|
|
473
|
+
},
|
|
474
|
+
variant: "action",
|
|
475
|
+
before: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Icon, { name: "chevron_left" }),
|
|
476
|
+
children: "Back"
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
MenuOption.displayName = "MenuOption";
|
|
482
|
+
function highlightMatch(text, searchValue) {
|
|
483
|
+
if (!searchValue || !searchValue.trim()) {
|
|
484
|
+
return text;
|
|
485
|
+
}
|
|
486
|
+
const regex = new RegExp(`(${searchValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
|
|
487
|
+
const parts = text.split(regex);
|
|
488
|
+
return parts.map(
|
|
489
|
+
(part, index) => regex.test(part) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-bold", children: part }, index) : part
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/hooks/useKeydown.ts
|
|
494
|
+
var import_react3 = require("react");
|
|
495
|
+
function useKeydown(keys, isActive) {
|
|
496
|
+
function handleKeyDown(event) {
|
|
497
|
+
if (!Object.keys(keys).includes(event.key) && !Object.keys(keys).join("").includes("/"))
|
|
498
|
+
return;
|
|
499
|
+
Object.entries(keys).forEach(([key, handler]) => {
|
|
500
|
+
if (event.key !== key && !key.includes("/")) return;
|
|
501
|
+
if (key.includes("/") && key.replace("Space", " ").split("/").includes(event.key)) {
|
|
502
|
+
event.preventDefault();
|
|
503
|
+
handler(event);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (event.key === key) {
|
|
507
|
+
event.preventDefault();
|
|
508
|
+
handler(event);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
(0, import_react3.useEffect)(() => {
|
|
513
|
+
if (!isActive)
|
|
514
|
+
return document.removeEventListener("keydown", handleKeyDown);
|
|
515
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
516
|
+
return () => {
|
|
517
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
518
|
+
};
|
|
519
|
+
}, [keys, isActive]);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/components/NestedMenu.tsx
|
|
523
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
524
|
+
function NestedMenu(props) {
|
|
525
|
+
var _a;
|
|
526
|
+
const { onMenuClick, currentMenu } = props;
|
|
527
|
+
const [selectedIndex, setSelectedIndex] = (0, import_react4.useState)(0);
|
|
528
|
+
useKeydown(
|
|
529
|
+
{
|
|
530
|
+
Escape: () => {
|
|
531
|
+
setSelectedIndex(0);
|
|
532
|
+
handleMenuClick(null, true);
|
|
533
|
+
},
|
|
534
|
+
ArrowUp: () => {
|
|
535
|
+
setSelectedIndex((prev) => prev === 0 ? -1 : Math.max(prev - 1, 0));
|
|
536
|
+
},
|
|
537
|
+
ArrowDown: () => {
|
|
538
|
+
setSelectedIndex((prev) => {
|
|
539
|
+
var _a2;
|
|
540
|
+
const entriesLength = ((_a2 = currentMenu.subEntries) == null ? void 0 : _a2.length) || 0;
|
|
541
|
+
if (prev === entriesLength - 1) return 0;
|
|
542
|
+
return Math.min(prev + 1, (currentMenu.subEntries || []).length - 1);
|
|
543
|
+
});
|
|
544
|
+
},
|
|
545
|
+
// "Space" is replaced with " " to handle spacebar.
|
|
546
|
+
"Enter/Space": () => {
|
|
547
|
+
var _a2;
|
|
548
|
+
const item = (currentMenu.subEntries || [])[selectedIndex];
|
|
549
|
+
handleMenuClick(
|
|
550
|
+
item || currentMenu.previousMenu,
|
|
551
|
+
!item && currentMenu.previousMenu !== null
|
|
552
|
+
);
|
|
553
|
+
if (!item && !((_a2 = currentMenu.previousMenu) == null ? void 0 : _a2.previousMenu))
|
|
554
|
+
setSelectedIndex(0);
|
|
555
|
+
},
|
|
556
|
+
Backspace: () => {
|
|
557
|
+
var _a2, _b;
|
|
558
|
+
handleMenuClick((_a2 = currentMenu.previousMenu) != null ? _a2 : null, true);
|
|
559
|
+
if (!((_b = currentMenu.previousMenu) == null ? void 0 : _b.previousMenu)) setSelectedIndex(0);
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
true
|
|
563
|
+
);
|
|
564
|
+
function handleMenuClick(item, isPrevious = false) {
|
|
565
|
+
onMenuClick(
|
|
566
|
+
item && __spreadValues(__spreadValues({}, item), !isPrevious && { previousMenu: currentMenu })
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-col w-full", children: [
|
|
570
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
571
|
+
MenuOption,
|
|
572
|
+
{
|
|
573
|
+
variant: "action",
|
|
574
|
+
before: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon, { name: "chevron_left", className: "icon", size: 24 }),
|
|
575
|
+
onClick: () => {
|
|
576
|
+
var _a2;
|
|
577
|
+
return handleMenuClick((_a2 = currentMenu.previousMenu) != null ? _a2 : null, true);
|
|
578
|
+
},
|
|
579
|
+
selected: selectedIndex === -1,
|
|
580
|
+
children: currentMenu.previousMenu ? currentMenu.previousMenu.label : "Main Menu"
|
|
581
|
+
}
|
|
582
|
+
),
|
|
583
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(MenuOption, { variant: "action", children: (_a = currentMenu.title) != null ? _a : currentMenu.label }),
|
|
584
|
+
(currentMenu.subEntries || []).map((item, idx) => {
|
|
585
|
+
var _a2;
|
|
586
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
587
|
+
MenuOption,
|
|
588
|
+
{
|
|
589
|
+
variant: "normal",
|
|
590
|
+
onClick: () => handleMenuClick(item),
|
|
591
|
+
after: ((_a2 = item.subEntries) == null ? void 0 : _a2.length) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Icon, { name: "chevron_right", className: "icon", size: 24 }),
|
|
592
|
+
selected: selectedIndex === idx,
|
|
593
|
+
children: item.label
|
|
594
|
+
},
|
|
595
|
+
item.id
|
|
596
|
+
);
|
|
597
|
+
})
|
|
598
|
+
] });
|
|
599
|
+
}
|
|
600
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
601
|
+
0 && (module.exports = {
|
|
602
|
+
NestedMenu
|
|
603
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
MenuOption
|
|
4
|
+
} from "../chunk-YDREJNAS.js";
|
|
5
|
+
import "../chunk-SEKKGFM6.js";
|
|
6
|
+
import "../chunk-S7R37IUP.js";
|
|
7
|
+
import "../chunk-UIQ733QP.js";
|
|
8
|
+
import {
|
|
9
|
+
Icon
|
|
10
|
+
} from "../chunk-IGQVA7SC.js";
|
|
11
|
+
import "../chunk-RDLEIAQU.js";
|
|
12
|
+
import {
|
|
13
|
+
__spreadValues
|
|
14
|
+
} from "../chunk-ORMEWXMH.js";
|
|
15
|
+
|
|
16
|
+
// src/components/NestedMenu.tsx
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
|
|
19
|
+
// src/hooks/useKeydown.ts
|
|
20
|
+
import { useEffect } from "react";
|
|
21
|
+
function useKeydown(keys, isActive) {
|
|
22
|
+
function handleKeyDown(event) {
|
|
23
|
+
if (!Object.keys(keys).includes(event.key) && !Object.keys(keys).join("").includes("/"))
|
|
24
|
+
return;
|
|
25
|
+
Object.entries(keys).forEach(([key, handler]) => {
|
|
26
|
+
if (event.key !== key && !key.includes("/")) return;
|
|
27
|
+
if (key.includes("/") && key.replace("Space", " ").split("/").includes(event.key)) {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
handler(event);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (event.key === key) {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
handler(event);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!isActive)
|
|
40
|
+
return document.removeEventListener("keydown", handleKeyDown);
|
|
41
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
42
|
+
return () => {
|
|
43
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
44
|
+
};
|
|
45
|
+
}, [keys, isActive]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/components/NestedMenu.tsx
|
|
49
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
50
|
+
function NestedMenu(props) {
|
|
51
|
+
var _a;
|
|
52
|
+
const { onMenuClick, currentMenu } = props;
|
|
53
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
54
|
+
useKeydown(
|
|
55
|
+
{
|
|
56
|
+
Escape: () => {
|
|
57
|
+
setSelectedIndex(0);
|
|
58
|
+
handleMenuClick(null, true);
|
|
59
|
+
},
|
|
60
|
+
ArrowUp: () => {
|
|
61
|
+
setSelectedIndex((prev) => prev === 0 ? -1 : Math.max(prev - 1, 0));
|
|
62
|
+
},
|
|
63
|
+
ArrowDown: () => {
|
|
64
|
+
setSelectedIndex((prev) => {
|
|
65
|
+
var _a2;
|
|
66
|
+
const entriesLength = ((_a2 = currentMenu.subEntries) == null ? void 0 : _a2.length) || 0;
|
|
67
|
+
if (prev === entriesLength - 1) return 0;
|
|
68
|
+
return Math.min(prev + 1, (currentMenu.subEntries || []).length - 1);
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
// "Space" is replaced with " " to handle spacebar.
|
|
72
|
+
"Enter/Space": () => {
|
|
73
|
+
var _a2;
|
|
74
|
+
const item = (currentMenu.subEntries || [])[selectedIndex];
|
|
75
|
+
handleMenuClick(
|
|
76
|
+
item || currentMenu.previousMenu,
|
|
77
|
+
!item && currentMenu.previousMenu !== null
|
|
78
|
+
);
|
|
79
|
+
if (!item && !((_a2 = currentMenu.previousMenu) == null ? void 0 : _a2.previousMenu))
|
|
80
|
+
setSelectedIndex(0);
|
|
81
|
+
},
|
|
82
|
+
Backspace: () => {
|
|
83
|
+
var _a2, _b;
|
|
84
|
+
handleMenuClick((_a2 = currentMenu.previousMenu) != null ? _a2 : null, true);
|
|
85
|
+
if (!((_b = currentMenu.previousMenu) == null ? void 0 : _b.previousMenu)) setSelectedIndex(0);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
true
|
|
89
|
+
);
|
|
90
|
+
function handleMenuClick(item, isPrevious = false) {
|
|
91
|
+
onMenuClick(
|
|
92
|
+
item && __spreadValues(__spreadValues({}, item), !isPrevious && { previousMenu: currentMenu })
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col w-full", children: [
|
|
96
|
+
/* @__PURE__ */ jsx(
|
|
97
|
+
MenuOption,
|
|
98
|
+
{
|
|
99
|
+
variant: "action",
|
|
100
|
+
before: /* @__PURE__ */ jsx(Icon, { name: "chevron_left", className: "icon", size: 24 }),
|
|
101
|
+
onClick: () => {
|
|
102
|
+
var _a2;
|
|
103
|
+
return handleMenuClick((_a2 = currentMenu.previousMenu) != null ? _a2 : null, true);
|
|
104
|
+
},
|
|
105
|
+
selected: selectedIndex === -1,
|
|
106
|
+
children: currentMenu.previousMenu ? currentMenu.previousMenu.label : "Main Menu"
|
|
107
|
+
}
|
|
108
|
+
),
|
|
109
|
+
/* @__PURE__ */ jsx(MenuOption, { variant: "action", children: (_a = currentMenu.title) != null ? _a : currentMenu.label }),
|
|
110
|
+
(currentMenu.subEntries || []).map((item, idx) => {
|
|
111
|
+
var _a2;
|
|
112
|
+
return /* @__PURE__ */ jsx(
|
|
113
|
+
MenuOption,
|
|
114
|
+
{
|
|
115
|
+
variant: "normal",
|
|
116
|
+
onClick: () => handleMenuClick(item),
|
|
117
|
+
after: ((_a2 = item.subEntries) == null ? void 0 : _a2.length) && /* @__PURE__ */ jsx(Icon, { name: "chevron_right", className: "icon", size: 24 }),
|
|
118
|
+
selected: selectedIndex === idx,
|
|
119
|
+
children: item.label
|
|
120
|
+
},
|
|
121
|
+
item.id
|
|
122
|
+
);
|
|
123
|
+
})
|
|
124
|
+
] });
|
|
125
|
+
}
|
|
126
|
+
export {
|
|
127
|
+
NestedMenu
|
|
128
|
+
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Icon } from "./Icon";
|
|
5
|
+
import { MenuOption } from "./MenuOption";
|
|
6
|
+
import { useKeydown } from "../hooks";
|
|
7
|
+
|
|
8
|
+
export type NestedMenuEntry = {
|
|
9
|
+
id: string; // Unique identifier for the nested menu entry
|
|
10
|
+
label: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
icon?: string;
|
|
13
|
+
subEntries?: NestedMenuEntry[];
|
|
14
|
+
url?: string;
|
|
15
|
+
onClick?: () => void; // Optional click handler
|
|
16
|
+
previousMenu?: NestedMenuEntry | null; // Reference to the previous menu entry
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type NestedMenuProps = {
|
|
20
|
+
currentMenu: NestedMenuEntry; // Current menu entry, if any
|
|
21
|
+
onMenuClick: (entry: NestedMenuEntry | null) => void; // Callback when a menu item is clicked. tailing
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function NestedMenu(props: NestedMenuProps) {
|
|
25
|
+
const { onMenuClick, currentMenu } = props;
|
|
26
|
+
const [selectedIndex, setSelectedIndex] = useState<number>(0);
|
|
27
|
+
|
|
28
|
+
useKeydown(
|
|
29
|
+
{
|
|
30
|
+
Escape: () => {
|
|
31
|
+
setSelectedIndex(0);
|
|
32
|
+
handleMenuClick(null, true);
|
|
33
|
+
},
|
|
34
|
+
ArrowUp: () => {
|
|
35
|
+
setSelectedIndex((prev) => (prev === 0 ? -1 : Math.max(prev - 1, 0)));
|
|
36
|
+
},
|
|
37
|
+
ArrowDown: () => {
|
|
38
|
+
setSelectedIndex((prev) => {
|
|
39
|
+
const entriesLength = currentMenu.subEntries?.length || 0;
|
|
40
|
+
if (prev === entriesLength - 1) return 0;
|
|
41
|
+
return Math.min(prev + 1, (currentMenu.subEntries || []).length - 1);
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
// "Space" is replaced with " " to handle spacebar.
|
|
45
|
+
"Enter/Space": () => {
|
|
46
|
+
const item = (currentMenu.subEntries || [])[selectedIndex];
|
|
47
|
+
handleMenuClick(
|
|
48
|
+
item || currentMenu.previousMenu,
|
|
49
|
+
!item && currentMenu.previousMenu !== null,
|
|
50
|
+
);
|
|
51
|
+
if (!item && !currentMenu.previousMenu?.previousMenu)
|
|
52
|
+
setSelectedIndex(0);
|
|
53
|
+
},
|
|
54
|
+
Backspace: () => {
|
|
55
|
+
handleMenuClick(currentMenu.previousMenu ?? null, true);
|
|
56
|
+
if (!currentMenu.previousMenu?.previousMenu) setSelectedIndex(0);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
true,
|
|
60
|
+
); // Static active state as it unmounts if !currentMenu
|
|
61
|
+
|
|
62
|
+
function handleMenuClick(item: NestedMenuEntry | null, isPrevious = false) {
|
|
63
|
+
onMenuClick(
|
|
64
|
+
item && { ...item, ...(!isPrevious && { previousMenu: currentMenu }) },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex flex-col w-full">
|
|
70
|
+
<MenuOption
|
|
71
|
+
variant="action"
|
|
72
|
+
before={<Icon name="chevron_left" className="icon" size={24} />}
|
|
73
|
+
onClick={() => handleMenuClick(currentMenu.previousMenu ?? null, true)}
|
|
74
|
+
selected={selectedIndex === -1}
|
|
75
|
+
>
|
|
76
|
+
{currentMenu.previousMenu
|
|
77
|
+
? currentMenu.previousMenu.label
|
|
78
|
+
: "Main Menu"}
|
|
79
|
+
</MenuOption>
|
|
80
|
+
<MenuOption variant="action">
|
|
81
|
+
{currentMenu.title ?? currentMenu.label}
|
|
82
|
+
</MenuOption>
|
|
83
|
+
{(currentMenu.subEntries || []).map((item, idx) => (
|
|
84
|
+
<MenuOption
|
|
85
|
+
key={item.id}
|
|
86
|
+
variant="normal"
|
|
87
|
+
onClick={() => handleMenuClick(item)}
|
|
88
|
+
after={
|
|
89
|
+
item.subEntries?.length && (
|
|
90
|
+
<Icon name="chevron_right" className="icon" size={24} />
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
selected={selectedIndex === idx}
|
|
94
|
+
>
|
|
95
|
+
{item.label}
|
|
96
|
+
</MenuOption>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useKeydown } from "./useKeydown";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
type KeyHandler = (event: KeyboardEvent) => void;
|
|
4
|
+
|
|
5
|
+
export function useKeydown(
|
|
6
|
+
keys: Record<string, KeyHandler>,
|
|
7
|
+
isActive?: boolean,
|
|
8
|
+
): void {
|
|
9
|
+
function handleKeyDown(event: KeyboardEvent): void {
|
|
10
|
+
if (
|
|
11
|
+
!Object.keys(keys).includes(event.key) &&
|
|
12
|
+
!Object.keys(keys).join("").includes("/")
|
|
13
|
+
)
|
|
14
|
+
return;
|
|
15
|
+
Object.entries(keys).forEach(([key, handler]) => {
|
|
16
|
+
if (event.key !== key && !key.includes("/")) return;
|
|
17
|
+
// Match single keys or combinations like "Enter/Space"
|
|
18
|
+
if (
|
|
19
|
+
key.includes("/") &&
|
|
20
|
+
key.replace("Space", " ").split("/").includes(event.key)
|
|
21
|
+
) {
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
handler(event);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (event.key === key) {
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
handler(event);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!isActive)
|
|
35
|
+
return document.removeEventListener("keydown", handleKeyDown);
|
|
36
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
40
|
+
};
|
|
41
|
+
}, [keys, isActive]);
|
|
42
|
+
}
|