@velocis/dropdown1 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +159 -58
- package/dist/index.d.cts +18 -5
- package/dist/index.d.ts +18 -5
- package/dist/index.js +162 -61
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -43,6 +43,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
43
43
|
|
|
44
44
|
// src/Dropdown1.tsx
|
|
45
45
|
var import_core3 = require("@velocis/core");
|
|
46
|
+
var import_theme = require("@velocis/theme");
|
|
46
47
|
var import_react2 = require("react");
|
|
47
48
|
|
|
48
49
|
// src/Dropdown1.defaults.ts
|
|
@@ -58,7 +59,9 @@ var DROPDOWN1_ITEM_DEFAULTS = {
|
|
|
58
59
|
};
|
|
59
60
|
var DROPDOWN1_SUBMENU_DEFAULTS = {
|
|
60
61
|
contentWidth: "w-48",
|
|
61
|
-
hoverDelayMs: 100
|
|
62
|
+
hoverDelayMs: 100,
|
|
63
|
+
placement: "auto",
|
|
64
|
+
subMenuGap: 8
|
|
62
65
|
};
|
|
63
66
|
|
|
64
67
|
// src/Dropdown1.variants.ts
|
|
@@ -92,7 +95,7 @@ var dropdown1SubMenuTriggerVariants = (0, import_class_variance_authority.cva)(
|
|
|
92
95
|
"flex w-full cursor-default items-center justify-between px-4 py-2 text-sm transition-colors"
|
|
93
96
|
);
|
|
94
97
|
var dropdown1SubMenuContentVariants = (0, import_class_variance_authority.cva)(
|
|
95
|
-
"
|
|
98
|
+
"fixed w-48 rounded-velocis-md z-[10000]"
|
|
96
99
|
);
|
|
97
100
|
|
|
98
101
|
// src/context/Dropdown1Context.tsx
|
|
@@ -130,7 +133,7 @@ function resolveDropdown1Styles(overrides) {
|
|
|
130
133
|
content,
|
|
131
134
|
item: overrides?.item ?? dropdown1Styles.item,
|
|
132
135
|
subMenuTrigger: overrides?.subMenuTrigger ?? dropdown1Styles.subMenuTrigger,
|
|
133
|
-
subMenuContent: overrides?.subMenuContent ??
|
|
136
|
+
subMenuContent: overrides?.subMenuContent ?? dropdown1Styles.subMenuContent,
|
|
134
137
|
subMenuIcon: overrides?.subMenuIcon ?? dropdown1Styles.subMenuIcon
|
|
135
138
|
};
|
|
136
139
|
}
|
|
@@ -198,6 +201,8 @@ function Dropdown1Root({
|
|
|
198
201
|
defaultOpen = false,
|
|
199
202
|
onOpenChange,
|
|
200
203
|
styles: stylesProp,
|
|
204
|
+
surface,
|
|
205
|
+
triggerSurface,
|
|
201
206
|
triggerClassName,
|
|
202
207
|
contentClassName,
|
|
203
208
|
contentWidth = DROPDOWN1_DEFAULTS.contentWidth,
|
|
@@ -210,12 +215,21 @@ function Dropdown1Root({
|
|
|
210
215
|
contentTestId
|
|
211
216
|
}) {
|
|
212
217
|
const direction = (0, import_core3.useDirection)();
|
|
218
|
+
const { resolvedTheme } = (0, import_theme.useTheme)();
|
|
213
219
|
const { open, setOpen } = useControllableOpen(controlledOpen, defaultOpen, onOpenChange);
|
|
214
220
|
const styles = resolveDropdown1Styles(stylesProp);
|
|
215
|
-
const
|
|
221
|
+
const contentRef = (0, import_react2.useRef)(null);
|
|
216
222
|
const buttonRef = (0, import_react2.useRef)(null);
|
|
217
223
|
const [positionStyle, setPositionStyle] = (0, import_react2.useState)({});
|
|
218
224
|
const closeDropdown = (0, import_react2.useCallback)(() => setOpen(false), [setOpen]);
|
|
225
|
+
const isInsideDropdownPanel = (0, import_react2.useCallback)((node) => {
|
|
226
|
+
if (!node) return false;
|
|
227
|
+
if (contentRef.current?.contains(node)) return true;
|
|
228
|
+
if (node instanceof Element && node.closest("[data-velocis-dropdown1-submenu]")) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}, []);
|
|
219
233
|
const updatePosition = (0, import_react2.useCallback)(() => {
|
|
220
234
|
if (!buttonRef.current) return;
|
|
221
235
|
const rect = buttonRef.current.getBoundingClientRect();
|
|
@@ -237,7 +251,7 @@ function Dropdown1Root({
|
|
|
237
251
|
(0, import_react2.useEffect)(() => {
|
|
238
252
|
if (!open) return;
|
|
239
253
|
const handleClickOutside = (event) => {
|
|
240
|
-
if (
|
|
254
|
+
if (buttonRef.current && !buttonRef.current.contains(event.target) && !isInsideDropdownPanel(event.target)) {
|
|
241
255
|
setOpen(false);
|
|
242
256
|
}
|
|
243
257
|
};
|
|
@@ -249,7 +263,7 @@ function Dropdown1Root({
|
|
|
249
263
|
const handleScroll = (event) => {
|
|
250
264
|
if (!closeOnScroll) return;
|
|
251
265
|
const target = event.target;
|
|
252
|
-
if (
|
|
266
|
+
if (target && !isInsideDropdownPanel(target) && !buttonRef.current?.contains(target)) {
|
|
253
267
|
setOpen(false);
|
|
254
268
|
}
|
|
255
269
|
};
|
|
@@ -261,22 +275,26 @@ function Dropdown1Root({
|
|
|
261
275
|
document.removeEventListener("keydown", handleKeyDown);
|
|
262
276
|
document.removeEventListener("scroll", handleScroll, true);
|
|
263
277
|
};
|
|
264
|
-
}, [closeOnScroll, open, setOpen]);
|
|
265
|
-
const
|
|
278
|
+
}, [closeOnScroll, isInsideDropdownPanel, open, setOpen]);
|
|
279
|
+
const contentSurfaceProps = (0, import_theme.applySurface)(surface, resolvedTheme, {
|
|
280
|
+
className: (0, import_core3.cn)(
|
|
281
|
+
dropdown1ContentVariants(),
|
|
282
|
+
styles.content,
|
|
283
|
+
!fullWidth && contentWidth,
|
|
284
|
+
maxHeight && "overflow-y-auto",
|
|
285
|
+
contentClassName
|
|
286
|
+
)
|
|
287
|
+
});
|
|
288
|
+
const dropdownContent = open ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dropdown1Context.Provider, { value: { closeDropdown, styles, contentRef }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
266
289
|
"div",
|
|
267
290
|
{
|
|
268
|
-
ref:
|
|
291
|
+
ref: contentRef,
|
|
269
292
|
role: "menu",
|
|
270
293
|
"data-testid": contentTestId,
|
|
271
294
|
dir: direction,
|
|
272
|
-
|
|
273
|
-
dropdown1ContentVariants(),
|
|
274
|
-
styles.content,
|
|
275
|
-
!fullWidth && contentWidth,
|
|
276
|
-
maxHeight && "overflow-y-auto",
|
|
277
|
-
contentClassName
|
|
278
|
-
),
|
|
295
|
+
...contentSurfaceProps,
|
|
279
296
|
style: {
|
|
297
|
+
...contentSurfaceProps.style,
|
|
280
298
|
...positionStyle,
|
|
281
299
|
zIndex: contentZIndex,
|
|
282
300
|
maxHeight: maxHeight || void 0
|
|
@@ -284,6 +302,13 @@ function Dropdown1Root({
|
|
|
284
302
|
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "py-1", children })
|
|
285
303
|
}
|
|
286
304
|
) }) : null;
|
|
305
|
+
const triggerSurfaceProps = (0, import_theme.applySurface)(triggerSurface, resolvedTheme, {
|
|
306
|
+
className: (0, import_core3.cn)(
|
|
307
|
+
dropdown1TriggerVariants({ fullWidth, disabled }),
|
|
308
|
+
styles.trigger,
|
|
309
|
+
triggerClassName
|
|
310
|
+
)
|
|
311
|
+
});
|
|
287
312
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
288
313
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
289
314
|
"div",
|
|
@@ -302,11 +327,7 @@ function Dropdown1Root({
|
|
|
302
327
|
"aria-haspopup": "menu",
|
|
303
328
|
onClick: () => !disabled && setOpen(!open),
|
|
304
329
|
"data-testid": testId,
|
|
305
|
-
|
|
306
|
-
dropdown1TriggerVariants({ fullWidth, disabled }),
|
|
307
|
-
styles.trigger,
|
|
308
|
-
triggerClassName
|
|
309
|
-
),
|
|
330
|
+
...triggerSurfaceProps,
|
|
310
331
|
children: [
|
|
311
332
|
trigger,
|
|
312
333
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ChevronDownIcon, { className: styles.triggerIcon })
|
|
@@ -358,28 +379,101 @@ function Dropdown1Item({
|
|
|
358
379
|
}
|
|
359
380
|
|
|
360
381
|
// src/components/Dropdown1SubMenu.tsx
|
|
361
|
-
var
|
|
382
|
+
var import_core6 = require("@velocis/core");
|
|
383
|
+
var import_theme2 = require("@velocis/theme");
|
|
362
384
|
var import_react3 = require("react");
|
|
385
|
+
|
|
386
|
+
// src/positioning/computeSubMenuStyle.ts
|
|
387
|
+
var import_core5 = require("@velocis/core");
|
|
388
|
+
function resolvePlacement(placement, direction) {
|
|
389
|
+
if (placement === "auto") {
|
|
390
|
+
return (0, import_core5.isRTL)(direction) ? "left" : "right";
|
|
391
|
+
}
|
|
392
|
+
return placement;
|
|
393
|
+
}
|
|
394
|
+
function computeSubMenuStyle({
|
|
395
|
+
placement,
|
|
396
|
+
direction,
|
|
397
|
+
contentRect,
|
|
398
|
+
triggerRect,
|
|
399
|
+
subMenuWidth,
|
|
400
|
+
gap = 8
|
|
401
|
+
}) {
|
|
402
|
+
const resolved = resolvePlacement(placement, direction);
|
|
403
|
+
const margin = 8;
|
|
404
|
+
const vw = window.innerWidth;
|
|
405
|
+
const vh = window.innerHeight;
|
|
406
|
+
if (resolved === "center") {
|
|
407
|
+
const top2 = contentRect.bottom + gap;
|
|
408
|
+
const left2 = contentRect.left + (contentRect.width - subMenuWidth) / 2;
|
|
409
|
+
return {
|
|
410
|
+
position: "fixed",
|
|
411
|
+
top: Math.max(margin, Math.min(top2, vh - margin)),
|
|
412
|
+
left: Math.max(margin, Math.min(left2, vw - subMenuWidth - margin)),
|
|
413
|
+
width: subMenuWidth
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const top = triggerRect.top;
|
|
417
|
+
let left;
|
|
418
|
+
if (resolved === "left") {
|
|
419
|
+
left = contentRect.left - subMenuWidth - gap;
|
|
420
|
+
} else {
|
|
421
|
+
left = contentRect.right + gap;
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
position: "fixed",
|
|
425
|
+
top: Math.max(margin, Math.min(top, vh - margin)),
|
|
426
|
+
left: Math.max(margin, Math.min(left, vw - subMenuWidth - margin)),
|
|
427
|
+
width: subMenuWidth
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/components/Dropdown1SubMenu.tsx
|
|
363
432
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
364
433
|
function Dropdown1SubMenu({
|
|
365
434
|
trigger,
|
|
366
435
|
children,
|
|
436
|
+
placement = DROPDOWN1_SUBMENU_DEFAULTS.placement,
|
|
367
437
|
contentWidth = DROPDOWN1_SUBMENU_DEFAULTS.contentWidth,
|
|
368
438
|
styles: stylesProp,
|
|
439
|
+
surface,
|
|
369
440
|
className
|
|
370
441
|
}) {
|
|
442
|
+
const direction = (0, import_core6.useDirection)();
|
|
443
|
+
const { resolvedTheme } = (0, import_theme2.useTheme)();
|
|
371
444
|
const context = useDropdown1Context();
|
|
372
445
|
const subMenuTriggerStyles = stylesProp?.subMenuTrigger ?? context?.styles.subMenuTrigger;
|
|
373
446
|
const subMenuContentStyles = stylesProp?.subMenuContent ?? context?.styles.subMenuContent;
|
|
374
447
|
const subMenuIconStyles = stylesProp?.subMenuIcon ?? context?.styles.subMenuIcon;
|
|
375
448
|
const [isOpen, setIsOpen] = (0, import_react3.useState)(false);
|
|
449
|
+
const [positionStyle, setPositionStyle] = (0, import_react3.useState)({});
|
|
376
450
|
const timeoutRef = (0, import_react3.useRef)(null);
|
|
377
|
-
const
|
|
378
|
-
const
|
|
451
|
+
const triggerRef = (0, import_react3.useRef)(null);
|
|
452
|
+
const panelRef = (0, import_react3.useRef)(null);
|
|
453
|
+
const updatePosition = (0, import_react3.useCallback)(() => {
|
|
454
|
+
const contentEl = context?.contentRef.current;
|
|
455
|
+
const triggerEl = triggerRef.current;
|
|
456
|
+
if (!contentEl || !triggerEl) return;
|
|
457
|
+
const subMenuWidth = parseContentWidthClass(contentWidth) ?? panelRef.current?.getBoundingClientRect().width ?? 192;
|
|
458
|
+
setPositionStyle(
|
|
459
|
+
computeSubMenuStyle({
|
|
460
|
+
placement,
|
|
461
|
+
direction,
|
|
462
|
+
contentRect: contentEl.getBoundingClientRect(),
|
|
463
|
+
triggerRect: triggerEl.getBoundingClientRect(),
|
|
464
|
+
subMenuWidth,
|
|
465
|
+
gap: DROPDOWN1_SUBMENU_DEFAULTS.subMenuGap
|
|
466
|
+
})
|
|
467
|
+
);
|
|
468
|
+
}, [contentWidth, context?.contentRef, direction, placement]);
|
|
469
|
+
const clearCloseTimeout = () => {
|
|
379
470
|
if (timeoutRef.current) {
|
|
380
471
|
clearTimeout(timeoutRef.current);
|
|
381
472
|
timeoutRef.current = null;
|
|
382
473
|
}
|
|
474
|
+
};
|
|
475
|
+
const handleMouseEnter = () => {
|
|
476
|
+
clearCloseTimeout();
|
|
383
477
|
setIsOpen(true);
|
|
384
478
|
};
|
|
385
479
|
const handleMouseLeave = () => {
|
|
@@ -387,6 +481,11 @@ function Dropdown1SubMenu({
|
|
|
387
481
|
setIsOpen(false);
|
|
388
482
|
}, DROPDOWN1_SUBMENU_DEFAULTS.hoverDelayMs);
|
|
389
483
|
};
|
|
484
|
+
(0, import_react3.useEffect)(() => {
|
|
485
|
+
if (isOpen) {
|
|
486
|
+
updatePosition();
|
|
487
|
+
}
|
|
488
|
+
}, [isOpen, updatePosition]);
|
|
390
489
|
(0, import_react3.useEffect)(() => {
|
|
391
490
|
return () => {
|
|
392
491
|
if (timeoutRef.current) {
|
|
@@ -394,45 +493,47 @@ function Dropdown1SubMenu({
|
|
|
394
493
|
}
|
|
395
494
|
};
|
|
396
495
|
}, []);
|
|
397
|
-
|
|
496
|
+
const panelSurfaceProps = (0, import_theme2.applySurface)(surface, resolvedTheme, {
|
|
497
|
+
className: (0, import_core6.cn)(
|
|
498
|
+
dropdown1SubMenuContentVariants(),
|
|
499
|
+
subMenuContentStyles,
|
|
500
|
+
contentWidth !== "w-48" && contentWidth,
|
|
501
|
+
className
|
|
502
|
+
)
|
|
503
|
+
});
|
|
504
|
+
const subMenuPanel = isOpen ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
398
505
|
"div",
|
|
399
506
|
{
|
|
400
|
-
ref:
|
|
401
|
-
|
|
507
|
+
ref: panelRef,
|
|
508
|
+
role: "menu",
|
|
509
|
+
"data-velocis-dropdown1-submenu": "",
|
|
510
|
+
...panelSurfaceProps,
|
|
511
|
+
style: {
|
|
512
|
+
...panelSurfaceProps.style,
|
|
513
|
+
...positionStyle
|
|
514
|
+
},
|
|
402
515
|
onMouseEnter: handleMouseEnter,
|
|
403
516
|
onMouseLeave: handleMouseLeave,
|
|
404
|
-
children:
|
|
405
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
406
|
-
"div",
|
|
407
|
-
{
|
|
408
|
-
role: "menuitem",
|
|
409
|
-
"aria-haspopup": "menu",
|
|
410
|
-
"aria-expanded": isOpen,
|
|
411
|
-
className: (0, import_core5.cn)(dropdown1SubMenuTriggerVariants(), subMenuTriggerStyles),
|
|
412
|
-
children: [
|
|
413
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "flex items-center gap-2", children: trigger }),
|
|
414
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChevronSubMenuIcon, { open: isOpen, className: (0, import_core5.cn)("transition-transform", subMenuIconStyles) })
|
|
415
|
-
]
|
|
416
|
-
}
|
|
417
|
-
),
|
|
418
|
-
isOpen && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
419
|
-
"div",
|
|
420
|
-
{
|
|
421
|
-
role: "menu",
|
|
422
|
-
className: (0, import_core5.cn)(
|
|
423
|
-
dropdown1SubMenuContentVariants(),
|
|
424
|
-
subMenuContentStyles,
|
|
425
|
-
contentWidth !== "w-48" && contentWidth,
|
|
426
|
-
className
|
|
427
|
-
),
|
|
428
|
-
onMouseEnter: handleMouseEnter,
|
|
429
|
-
onMouseLeave: handleMouseLeave,
|
|
430
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "py-1", children })
|
|
431
|
-
}
|
|
432
|
-
)
|
|
433
|
-
]
|
|
517
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "py-1", children })
|
|
434
518
|
}
|
|
435
|
-
);
|
|
519
|
+
) : null;
|
|
520
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "relative w-full", onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [
|
|
521
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
522
|
+
"div",
|
|
523
|
+
{
|
|
524
|
+
ref: triggerRef,
|
|
525
|
+
role: "menuitem",
|
|
526
|
+
"aria-haspopup": "menu",
|
|
527
|
+
"aria-expanded": isOpen,
|
|
528
|
+
className: (0, import_core6.cn)(dropdown1SubMenuTriggerVariants(), subMenuTriggerStyles),
|
|
529
|
+
children: [
|
|
530
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "flex items-center gap-2", children: trigger }),
|
|
531
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChevronSubMenuIcon, { open: isOpen, className: (0, import_core6.cn)("transition-transform", subMenuIconStyles) })
|
|
532
|
+
]
|
|
533
|
+
}
|
|
534
|
+
),
|
|
535
|
+
isOpen && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_core6.Portal, { children: subMenuPanel })
|
|
536
|
+
] });
|
|
436
537
|
}
|
|
437
538
|
|
|
438
539
|
// src/hooks/useDropdown1.ts
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { ReactNode, RefObject } from 'react';
|
|
3
|
+
import { VelocisSurfaceConfig } from '@velocis/theme';
|
|
3
4
|
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
4
5
|
|
|
5
6
|
type Dropdown1Styles = {
|
|
@@ -13,7 +14,7 @@ type Dropdown1Styles = {
|
|
|
13
14
|
item?: string;
|
|
14
15
|
/** Sub-menu row — text + hover bg/text */
|
|
15
16
|
subMenuTrigger?: string;
|
|
16
|
-
/** Sub-menu panel —
|
|
17
|
+
/** Sub-menu panel — independent default from `content` */
|
|
17
18
|
subMenuContent?: string;
|
|
18
19
|
/** Chevron on sub-menu row */
|
|
19
20
|
subMenuIcon?: string;
|
|
@@ -38,6 +39,10 @@ type Dropdown1Props = {
|
|
|
38
39
|
onOpenChange?: (open: boolean) => void;
|
|
39
40
|
/** Override coordinated bg/text/hover classes per layer */
|
|
40
41
|
styles?: Partial<Dropdown1Styles>;
|
|
42
|
+
/** Menu panel surface — background, tokens, light/dark lock */
|
|
43
|
+
surface?: VelocisSurfaceConfig;
|
|
44
|
+
/** Trigger button surface */
|
|
45
|
+
triggerSurface?: VelocisSurfaceConfig;
|
|
41
46
|
triggerClassName?: string;
|
|
42
47
|
contentClassName?: string;
|
|
43
48
|
contentWidth?: string;
|
|
@@ -58,20 +63,25 @@ type Dropdown1ItemProps = {
|
|
|
58
63
|
className?: string;
|
|
59
64
|
testId?: string;
|
|
60
65
|
};
|
|
66
|
+
type Dropdown1SubMenuPlacement = 'auto' | 'left' | 'right' | 'center';
|
|
61
67
|
type Dropdown1SubMenuProps = {
|
|
62
68
|
trigger: ReactNode;
|
|
63
69
|
children: ReactNode;
|
|
70
|
+
/** Where the sub-panel opens relative to the main menu panel */
|
|
71
|
+
placement?: Dropdown1SubMenuPlacement;
|
|
64
72
|
contentWidth?: string;
|
|
65
73
|
/** Override sub-menu colors (defaults to root styles) */
|
|
66
74
|
styles?: Pick<Dropdown1Styles, 'subMenuTrigger' | 'subMenuContent' | 'subMenuIcon'>;
|
|
75
|
+
/** Independent background/tokens — separate from root `surface` */
|
|
76
|
+
surface?: VelocisSurfaceConfig;
|
|
67
77
|
className?: string;
|
|
68
78
|
};
|
|
69
79
|
|
|
70
|
-
declare function Dropdown1Root({ trigger, children, open: controlledOpen, defaultOpen, onOpenChange, styles: stylesProp, triggerClassName, contentClassName, contentWidth, maxHeight, fullWidth, disabled, contentZIndex, closeOnScroll, testId, contentTestId, }: Dropdown1Props): react.JSX.Element;
|
|
80
|
+
declare function Dropdown1Root({ trigger, children, open: controlledOpen, defaultOpen, onOpenChange, styles: stylesProp, surface, triggerSurface, triggerClassName, contentClassName, contentWidth, maxHeight, fullWidth, disabled, contentZIndex, closeOnScroll, testId, contentTestId, }: Dropdown1Props): react.JSX.Element;
|
|
71
81
|
|
|
72
82
|
declare function Dropdown1Item({ children, onClick, closeOnSelect, styles: stylesProp, className, testId, }: Dropdown1ItemProps): react.JSX.Element;
|
|
73
83
|
|
|
74
|
-
declare function Dropdown1SubMenu({ trigger, children, contentWidth, styles: stylesProp, className, }: Dropdown1SubMenuProps): react.JSX.Element;
|
|
84
|
+
declare function Dropdown1SubMenu({ trigger, children, placement, contentWidth, styles: stylesProp, surface, className, }: Dropdown1SubMenuProps): react.JSX.Element;
|
|
75
85
|
|
|
76
86
|
type UseDropdown1Options = {
|
|
77
87
|
defaultOpen?: boolean;
|
|
@@ -86,6 +96,7 @@ declare function useDropdown1(options?: UseDropdown1Options): UseDropdown1Result
|
|
|
86
96
|
type Dropdown1ContextValue = {
|
|
87
97
|
closeDropdown: () => void;
|
|
88
98
|
styles: ResolvedDropdown1Styles;
|
|
99
|
+
contentRef: RefObject<HTMLDivElement | null>;
|
|
89
100
|
};
|
|
90
101
|
declare const Dropdown1Context: react.Context<Dropdown1ContextValue | null>;
|
|
91
102
|
declare function useDropdown1Context(): Dropdown1ContextValue | null;
|
|
@@ -103,6 +114,8 @@ declare const DROPDOWN1_ITEM_DEFAULTS: {
|
|
|
103
114
|
declare const DROPDOWN1_SUBMENU_DEFAULTS: {
|
|
104
115
|
readonly contentWidth: "w-48";
|
|
105
116
|
readonly hoverDelayMs: 100;
|
|
117
|
+
readonly placement: "auto";
|
|
118
|
+
readonly subMenuGap: 8;
|
|
106
119
|
};
|
|
107
120
|
|
|
108
121
|
/** Layout/structure only — colors come from `dropdown1Styles` + `styles` prop */
|
|
@@ -120,4 +133,4 @@ declare const Dropdown1: typeof Dropdown1Root & {
|
|
|
120
133
|
SubMenu: typeof Dropdown1SubMenu;
|
|
121
134
|
};
|
|
122
135
|
|
|
123
|
-
export { DROPDOWN1_DEFAULTS, DROPDOWN1_ITEM_DEFAULTS, DROPDOWN1_SUBMENU_DEFAULTS, Dropdown1, Dropdown1Context, type Dropdown1ContextValue, Dropdown1Item, type Dropdown1ItemProps, type Dropdown1Props, Dropdown1Root, type Dropdown1Styles, Dropdown1SubMenu, type Dropdown1SubMenuProps, type ResolvedDropdown1Styles, type UseDropdown1Options, type UseDropdown1Result, dropdown1ContentVariants, dropdown1ItemVariants, dropdown1Styles, dropdown1SubMenuContentVariants, dropdown1SubMenuTriggerVariants, dropdown1TriggerVariants, resolveDropdown1Styles, useDropdown1, useDropdown1Context };
|
|
136
|
+
export { DROPDOWN1_DEFAULTS, DROPDOWN1_ITEM_DEFAULTS, DROPDOWN1_SUBMENU_DEFAULTS, Dropdown1, Dropdown1Context, type Dropdown1ContextValue, Dropdown1Item, type Dropdown1ItemProps, type Dropdown1Props, Dropdown1Root, type Dropdown1Styles, Dropdown1SubMenu, type Dropdown1SubMenuPlacement, type Dropdown1SubMenuProps, type ResolvedDropdown1Styles, type UseDropdown1Options, type UseDropdown1Result, dropdown1ContentVariants, dropdown1ItemVariants, dropdown1Styles, dropdown1SubMenuContentVariants, dropdown1SubMenuTriggerVariants, dropdown1TriggerVariants, resolveDropdown1Styles, useDropdown1, useDropdown1Context };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { ReactNode, RefObject } from 'react';
|
|
3
|
+
import { VelocisSurfaceConfig } from '@velocis/theme';
|
|
3
4
|
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
4
5
|
|
|
5
6
|
type Dropdown1Styles = {
|
|
@@ -13,7 +14,7 @@ type Dropdown1Styles = {
|
|
|
13
14
|
item?: string;
|
|
14
15
|
/** Sub-menu row — text + hover bg/text */
|
|
15
16
|
subMenuTrigger?: string;
|
|
16
|
-
/** Sub-menu panel —
|
|
17
|
+
/** Sub-menu panel — independent default from `content` */
|
|
17
18
|
subMenuContent?: string;
|
|
18
19
|
/** Chevron on sub-menu row */
|
|
19
20
|
subMenuIcon?: string;
|
|
@@ -38,6 +39,10 @@ type Dropdown1Props = {
|
|
|
38
39
|
onOpenChange?: (open: boolean) => void;
|
|
39
40
|
/** Override coordinated bg/text/hover classes per layer */
|
|
40
41
|
styles?: Partial<Dropdown1Styles>;
|
|
42
|
+
/** Menu panel surface — background, tokens, light/dark lock */
|
|
43
|
+
surface?: VelocisSurfaceConfig;
|
|
44
|
+
/** Trigger button surface */
|
|
45
|
+
triggerSurface?: VelocisSurfaceConfig;
|
|
41
46
|
triggerClassName?: string;
|
|
42
47
|
contentClassName?: string;
|
|
43
48
|
contentWidth?: string;
|
|
@@ -58,20 +63,25 @@ type Dropdown1ItemProps = {
|
|
|
58
63
|
className?: string;
|
|
59
64
|
testId?: string;
|
|
60
65
|
};
|
|
66
|
+
type Dropdown1SubMenuPlacement = 'auto' | 'left' | 'right' | 'center';
|
|
61
67
|
type Dropdown1SubMenuProps = {
|
|
62
68
|
trigger: ReactNode;
|
|
63
69
|
children: ReactNode;
|
|
70
|
+
/** Where the sub-panel opens relative to the main menu panel */
|
|
71
|
+
placement?: Dropdown1SubMenuPlacement;
|
|
64
72
|
contentWidth?: string;
|
|
65
73
|
/** Override sub-menu colors (defaults to root styles) */
|
|
66
74
|
styles?: Pick<Dropdown1Styles, 'subMenuTrigger' | 'subMenuContent' | 'subMenuIcon'>;
|
|
75
|
+
/** Independent background/tokens — separate from root `surface` */
|
|
76
|
+
surface?: VelocisSurfaceConfig;
|
|
67
77
|
className?: string;
|
|
68
78
|
};
|
|
69
79
|
|
|
70
|
-
declare function Dropdown1Root({ trigger, children, open: controlledOpen, defaultOpen, onOpenChange, styles: stylesProp, triggerClassName, contentClassName, contentWidth, maxHeight, fullWidth, disabled, contentZIndex, closeOnScroll, testId, contentTestId, }: Dropdown1Props): react.JSX.Element;
|
|
80
|
+
declare function Dropdown1Root({ trigger, children, open: controlledOpen, defaultOpen, onOpenChange, styles: stylesProp, surface, triggerSurface, triggerClassName, contentClassName, contentWidth, maxHeight, fullWidth, disabled, contentZIndex, closeOnScroll, testId, contentTestId, }: Dropdown1Props): react.JSX.Element;
|
|
71
81
|
|
|
72
82
|
declare function Dropdown1Item({ children, onClick, closeOnSelect, styles: stylesProp, className, testId, }: Dropdown1ItemProps): react.JSX.Element;
|
|
73
83
|
|
|
74
|
-
declare function Dropdown1SubMenu({ trigger, children, contentWidth, styles: stylesProp, className, }: Dropdown1SubMenuProps): react.JSX.Element;
|
|
84
|
+
declare function Dropdown1SubMenu({ trigger, children, placement, contentWidth, styles: stylesProp, surface, className, }: Dropdown1SubMenuProps): react.JSX.Element;
|
|
75
85
|
|
|
76
86
|
type UseDropdown1Options = {
|
|
77
87
|
defaultOpen?: boolean;
|
|
@@ -86,6 +96,7 @@ declare function useDropdown1(options?: UseDropdown1Options): UseDropdown1Result
|
|
|
86
96
|
type Dropdown1ContextValue = {
|
|
87
97
|
closeDropdown: () => void;
|
|
88
98
|
styles: ResolvedDropdown1Styles;
|
|
99
|
+
contentRef: RefObject<HTMLDivElement | null>;
|
|
89
100
|
};
|
|
90
101
|
declare const Dropdown1Context: react.Context<Dropdown1ContextValue | null>;
|
|
91
102
|
declare function useDropdown1Context(): Dropdown1ContextValue | null;
|
|
@@ -103,6 +114,8 @@ declare const DROPDOWN1_ITEM_DEFAULTS: {
|
|
|
103
114
|
declare const DROPDOWN1_SUBMENU_DEFAULTS: {
|
|
104
115
|
readonly contentWidth: "w-48";
|
|
105
116
|
readonly hoverDelayMs: 100;
|
|
117
|
+
readonly placement: "auto";
|
|
118
|
+
readonly subMenuGap: 8;
|
|
106
119
|
};
|
|
107
120
|
|
|
108
121
|
/** Layout/structure only — colors come from `dropdown1Styles` + `styles` prop */
|
|
@@ -120,4 +133,4 @@ declare const Dropdown1: typeof Dropdown1Root & {
|
|
|
120
133
|
SubMenu: typeof Dropdown1SubMenu;
|
|
121
134
|
};
|
|
122
135
|
|
|
123
|
-
export { DROPDOWN1_DEFAULTS, DROPDOWN1_ITEM_DEFAULTS, DROPDOWN1_SUBMENU_DEFAULTS, Dropdown1, Dropdown1Context, type Dropdown1ContextValue, Dropdown1Item, type Dropdown1ItemProps, type Dropdown1Props, Dropdown1Root, type Dropdown1Styles, Dropdown1SubMenu, type Dropdown1SubMenuProps, type ResolvedDropdown1Styles, type UseDropdown1Options, type UseDropdown1Result, dropdown1ContentVariants, dropdown1ItemVariants, dropdown1Styles, dropdown1SubMenuContentVariants, dropdown1SubMenuTriggerVariants, dropdown1TriggerVariants, resolveDropdown1Styles, useDropdown1, useDropdown1Context };
|
|
136
|
+
export { DROPDOWN1_DEFAULTS, DROPDOWN1_ITEM_DEFAULTS, DROPDOWN1_SUBMENU_DEFAULTS, Dropdown1, Dropdown1Context, type Dropdown1ContextValue, Dropdown1Item, type Dropdown1ItemProps, type Dropdown1Props, Dropdown1Root, type Dropdown1Styles, Dropdown1SubMenu, type Dropdown1SubMenuPlacement, type Dropdown1SubMenuProps, type ResolvedDropdown1Styles, type UseDropdown1Options, type UseDropdown1Result, dropdown1ContentVariants, dropdown1ItemVariants, dropdown1Styles, dropdown1SubMenuContentVariants, dropdown1SubMenuTriggerVariants, dropdown1TriggerVariants, resolveDropdown1Styles, useDropdown1, useDropdown1Context };
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/Dropdown1.tsx
|
|
4
4
|
import { cn as cn2, Portal, useDirection } from "@velocis/core";
|
|
5
|
+
import { applySurface, useTheme } from "@velocis/theme";
|
|
5
6
|
import {
|
|
6
7
|
useCallback,
|
|
7
8
|
useEffect,
|
|
@@ -22,7 +23,9 @@ var DROPDOWN1_ITEM_DEFAULTS = {
|
|
|
22
23
|
};
|
|
23
24
|
var DROPDOWN1_SUBMENU_DEFAULTS = {
|
|
24
25
|
contentWidth: "w-48",
|
|
25
|
-
hoverDelayMs: 100
|
|
26
|
+
hoverDelayMs: 100,
|
|
27
|
+
placement: "auto",
|
|
28
|
+
subMenuGap: 8
|
|
26
29
|
};
|
|
27
30
|
|
|
28
31
|
// src/Dropdown1.variants.ts
|
|
@@ -56,7 +59,7 @@ var dropdown1SubMenuTriggerVariants = cva(
|
|
|
56
59
|
"flex w-full cursor-default items-center justify-between px-4 py-2 text-sm transition-colors"
|
|
57
60
|
);
|
|
58
61
|
var dropdown1SubMenuContentVariants = cva(
|
|
59
|
-
"
|
|
62
|
+
"fixed w-48 rounded-velocis-md z-[10000]"
|
|
60
63
|
);
|
|
61
64
|
|
|
62
65
|
// src/context/Dropdown1Context.tsx
|
|
@@ -94,7 +97,7 @@ function resolveDropdown1Styles(overrides) {
|
|
|
94
97
|
content,
|
|
95
98
|
item: overrides?.item ?? dropdown1Styles.item,
|
|
96
99
|
subMenuTrigger: overrides?.subMenuTrigger ?? dropdown1Styles.subMenuTrigger,
|
|
97
|
-
subMenuContent: overrides?.subMenuContent ??
|
|
100
|
+
subMenuContent: overrides?.subMenuContent ?? dropdown1Styles.subMenuContent,
|
|
98
101
|
subMenuIcon: overrides?.subMenuIcon ?? dropdown1Styles.subMenuIcon
|
|
99
102
|
};
|
|
100
103
|
}
|
|
@@ -162,6 +165,8 @@ function Dropdown1Root({
|
|
|
162
165
|
defaultOpen = false,
|
|
163
166
|
onOpenChange,
|
|
164
167
|
styles: stylesProp,
|
|
168
|
+
surface,
|
|
169
|
+
triggerSurface,
|
|
165
170
|
triggerClassName,
|
|
166
171
|
contentClassName,
|
|
167
172
|
contentWidth = DROPDOWN1_DEFAULTS.contentWidth,
|
|
@@ -174,12 +179,21 @@ function Dropdown1Root({
|
|
|
174
179
|
contentTestId
|
|
175
180
|
}) {
|
|
176
181
|
const direction = useDirection();
|
|
182
|
+
const { resolvedTheme } = useTheme();
|
|
177
183
|
const { open, setOpen } = useControllableOpen(controlledOpen, defaultOpen, onOpenChange);
|
|
178
184
|
const styles = resolveDropdown1Styles(stylesProp);
|
|
179
|
-
const
|
|
185
|
+
const contentRef = useRef(null);
|
|
180
186
|
const buttonRef = useRef(null);
|
|
181
187
|
const [positionStyle, setPositionStyle] = useState({});
|
|
182
188
|
const closeDropdown = useCallback(() => setOpen(false), [setOpen]);
|
|
189
|
+
const isInsideDropdownPanel = useCallback((node) => {
|
|
190
|
+
if (!node) return false;
|
|
191
|
+
if (contentRef.current?.contains(node)) return true;
|
|
192
|
+
if (node instanceof Element && node.closest("[data-velocis-dropdown1-submenu]")) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}, []);
|
|
183
197
|
const updatePosition = useCallback(() => {
|
|
184
198
|
if (!buttonRef.current) return;
|
|
185
199
|
const rect = buttonRef.current.getBoundingClientRect();
|
|
@@ -201,7 +215,7 @@ function Dropdown1Root({
|
|
|
201
215
|
useEffect(() => {
|
|
202
216
|
if (!open) return;
|
|
203
217
|
const handleClickOutside = (event) => {
|
|
204
|
-
if (
|
|
218
|
+
if (buttonRef.current && !buttonRef.current.contains(event.target) && !isInsideDropdownPanel(event.target)) {
|
|
205
219
|
setOpen(false);
|
|
206
220
|
}
|
|
207
221
|
};
|
|
@@ -213,7 +227,7 @@ function Dropdown1Root({
|
|
|
213
227
|
const handleScroll = (event) => {
|
|
214
228
|
if (!closeOnScroll) return;
|
|
215
229
|
const target = event.target;
|
|
216
|
-
if (
|
|
230
|
+
if (target && !isInsideDropdownPanel(target) && !buttonRef.current?.contains(target)) {
|
|
217
231
|
setOpen(false);
|
|
218
232
|
}
|
|
219
233
|
};
|
|
@@ -225,22 +239,26 @@ function Dropdown1Root({
|
|
|
225
239
|
document.removeEventListener("keydown", handleKeyDown);
|
|
226
240
|
document.removeEventListener("scroll", handleScroll, true);
|
|
227
241
|
};
|
|
228
|
-
}, [closeOnScroll, open, setOpen]);
|
|
229
|
-
const
|
|
242
|
+
}, [closeOnScroll, isInsideDropdownPanel, open, setOpen]);
|
|
243
|
+
const contentSurfaceProps = applySurface(surface, resolvedTheme, {
|
|
244
|
+
className: cn2(
|
|
245
|
+
dropdown1ContentVariants(),
|
|
246
|
+
styles.content,
|
|
247
|
+
!fullWidth && contentWidth,
|
|
248
|
+
maxHeight && "overflow-y-auto",
|
|
249
|
+
contentClassName
|
|
250
|
+
)
|
|
251
|
+
});
|
|
252
|
+
const dropdownContent = open ? /* @__PURE__ */ jsx2(Dropdown1Context.Provider, { value: { closeDropdown, styles, contentRef }, children: /* @__PURE__ */ jsx2(
|
|
230
253
|
"div",
|
|
231
254
|
{
|
|
232
|
-
ref:
|
|
255
|
+
ref: contentRef,
|
|
233
256
|
role: "menu",
|
|
234
257
|
"data-testid": contentTestId,
|
|
235
258
|
dir: direction,
|
|
236
|
-
|
|
237
|
-
dropdown1ContentVariants(),
|
|
238
|
-
styles.content,
|
|
239
|
-
!fullWidth && contentWidth,
|
|
240
|
-
maxHeight && "overflow-y-auto",
|
|
241
|
-
contentClassName
|
|
242
|
-
),
|
|
259
|
+
...contentSurfaceProps,
|
|
243
260
|
style: {
|
|
261
|
+
...contentSurfaceProps.style,
|
|
244
262
|
...positionStyle,
|
|
245
263
|
zIndex: contentZIndex,
|
|
246
264
|
maxHeight: maxHeight || void 0
|
|
@@ -248,6 +266,13 @@ function Dropdown1Root({
|
|
|
248
266
|
children: /* @__PURE__ */ jsx2("div", { className: "py-1", children })
|
|
249
267
|
}
|
|
250
268
|
) }) : null;
|
|
269
|
+
const triggerSurfaceProps = applySurface(triggerSurface, resolvedTheme, {
|
|
270
|
+
className: cn2(
|
|
271
|
+
dropdown1TriggerVariants({ fullWidth, disabled }),
|
|
272
|
+
styles.trigger,
|
|
273
|
+
triggerClassName
|
|
274
|
+
)
|
|
275
|
+
});
|
|
251
276
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
252
277
|
/* @__PURE__ */ jsx2(
|
|
253
278
|
"div",
|
|
@@ -266,11 +291,7 @@ function Dropdown1Root({
|
|
|
266
291
|
"aria-haspopup": "menu",
|
|
267
292
|
onClick: () => !disabled && setOpen(!open),
|
|
268
293
|
"data-testid": testId,
|
|
269
|
-
|
|
270
|
-
dropdown1TriggerVariants({ fullWidth, disabled }),
|
|
271
|
-
styles.trigger,
|
|
272
|
-
triggerClassName
|
|
273
|
-
),
|
|
294
|
+
...triggerSurfaceProps,
|
|
274
295
|
children: [
|
|
275
296
|
trigger,
|
|
276
297
|
/* @__PURE__ */ jsx2(ChevronDownIcon, { className: styles.triggerIcon })
|
|
@@ -322,28 +343,101 @@ function Dropdown1Item({
|
|
|
322
343
|
}
|
|
323
344
|
|
|
324
345
|
// src/components/Dropdown1SubMenu.tsx
|
|
325
|
-
import { cn as cn4 } from "@velocis/core";
|
|
326
|
-
import {
|
|
346
|
+
import { cn as cn4, Portal as Portal2, useDirection as useDirection2 } from "@velocis/core";
|
|
347
|
+
import { applySurface as applySurface2, useTheme as useTheme2 } from "@velocis/theme";
|
|
348
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
349
|
+
|
|
350
|
+
// src/positioning/computeSubMenuStyle.ts
|
|
351
|
+
import { isRTL as isRTL2 } from "@velocis/core";
|
|
352
|
+
function resolvePlacement(placement, direction) {
|
|
353
|
+
if (placement === "auto") {
|
|
354
|
+
return isRTL2(direction) ? "left" : "right";
|
|
355
|
+
}
|
|
356
|
+
return placement;
|
|
357
|
+
}
|
|
358
|
+
function computeSubMenuStyle({
|
|
359
|
+
placement,
|
|
360
|
+
direction,
|
|
361
|
+
contentRect,
|
|
362
|
+
triggerRect,
|
|
363
|
+
subMenuWidth,
|
|
364
|
+
gap = 8
|
|
365
|
+
}) {
|
|
366
|
+
const resolved = resolvePlacement(placement, direction);
|
|
367
|
+
const margin = 8;
|
|
368
|
+
const vw = window.innerWidth;
|
|
369
|
+
const vh = window.innerHeight;
|
|
370
|
+
if (resolved === "center") {
|
|
371
|
+
const top2 = contentRect.bottom + gap;
|
|
372
|
+
const left2 = contentRect.left + (contentRect.width - subMenuWidth) / 2;
|
|
373
|
+
return {
|
|
374
|
+
position: "fixed",
|
|
375
|
+
top: Math.max(margin, Math.min(top2, vh - margin)),
|
|
376
|
+
left: Math.max(margin, Math.min(left2, vw - subMenuWidth - margin)),
|
|
377
|
+
width: subMenuWidth
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const top = triggerRect.top;
|
|
381
|
+
let left;
|
|
382
|
+
if (resolved === "left") {
|
|
383
|
+
left = contentRect.left - subMenuWidth - gap;
|
|
384
|
+
} else {
|
|
385
|
+
left = contentRect.right + gap;
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
position: "fixed",
|
|
389
|
+
top: Math.max(margin, Math.min(top, vh - margin)),
|
|
390
|
+
left: Math.max(margin, Math.min(left, vw - subMenuWidth - margin)),
|
|
391
|
+
width: subMenuWidth
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/components/Dropdown1SubMenu.tsx
|
|
327
396
|
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
328
397
|
function Dropdown1SubMenu({
|
|
329
398
|
trigger,
|
|
330
399
|
children,
|
|
400
|
+
placement = DROPDOWN1_SUBMENU_DEFAULTS.placement,
|
|
331
401
|
contentWidth = DROPDOWN1_SUBMENU_DEFAULTS.contentWidth,
|
|
332
402
|
styles: stylesProp,
|
|
403
|
+
surface,
|
|
333
404
|
className
|
|
334
405
|
}) {
|
|
406
|
+
const direction = useDirection2();
|
|
407
|
+
const { resolvedTheme } = useTheme2();
|
|
335
408
|
const context = useDropdown1Context();
|
|
336
409
|
const subMenuTriggerStyles = stylesProp?.subMenuTrigger ?? context?.styles.subMenuTrigger;
|
|
337
410
|
const subMenuContentStyles = stylesProp?.subMenuContent ?? context?.styles.subMenuContent;
|
|
338
411
|
const subMenuIconStyles = stylesProp?.subMenuIcon ?? context?.styles.subMenuIcon;
|
|
339
412
|
const [isOpen, setIsOpen] = useState2(false);
|
|
413
|
+
const [positionStyle, setPositionStyle] = useState2({});
|
|
340
414
|
const timeoutRef = useRef2(null);
|
|
341
|
-
const
|
|
342
|
-
const
|
|
415
|
+
const triggerRef = useRef2(null);
|
|
416
|
+
const panelRef = useRef2(null);
|
|
417
|
+
const updatePosition = useCallback2(() => {
|
|
418
|
+
const contentEl = context?.contentRef.current;
|
|
419
|
+
const triggerEl = triggerRef.current;
|
|
420
|
+
if (!contentEl || !triggerEl) return;
|
|
421
|
+
const subMenuWidth = parseContentWidthClass(contentWidth) ?? panelRef.current?.getBoundingClientRect().width ?? 192;
|
|
422
|
+
setPositionStyle(
|
|
423
|
+
computeSubMenuStyle({
|
|
424
|
+
placement,
|
|
425
|
+
direction,
|
|
426
|
+
contentRect: contentEl.getBoundingClientRect(),
|
|
427
|
+
triggerRect: triggerEl.getBoundingClientRect(),
|
|
428
|
+
subMenuWidth,
|
|
429
|
+
gap: DROPDOWN1_SUBMENU_DEFAULTS.subMenuGap
|
|
430
|
+
})
|
|
431
|
+
);
|
|
432
|
+
}, [contentWidth, context?.contentRef, direction, placement]);
|
|
433
|
+
const clearCloseTimeout = () => {
|
|
343
434
|
if (timeoutRef.current) {
|
|
344
435
|
clearTimeout(timeoutRef.current);
|
|
345
436
|
timeoutRef.current = null;
|
|
346
437
|
}
|
|
438
|
+
};
|
|
439
|
+
const handleMouseEnter = () => {
|
|
440
|
+
clearCloseTimeout();
|
|
347
441
|
setIsOpen(true);
|
|
348
442
|
};
|
|
349
443
|
const handleMouseLeave = () => {
|
|
@@ -351,6 +445,11 @@ function Dropdown1SubMenu({
|
|
|
351
445
|
setIsOpen(false);
|
|
352
446
|
}, DROPDOWN1_SUBMENU_DEFAULTS.hoverDelayMs);
|
|
353
447
|
};
|
|
448
|
+
useEffect2(() => {
|
|
449
|
+
if (isOpen) {
|
|
450
|
+
updatePosition();
|
|
451
|
+
}
|
|
452
|
+
}, [isOpen, updatePosition]);
|
|
354
453
|
useEffect2(() => {
|
|
355
454
|
return () => {
|
|
356
455
|
if (timeoutRef.current) {
|
|
@@ -358,52 +457,54 @@ function Dropdown1SubMenu({
|
|
|
358
457
|
}
|
|
359
458
|
};
|
|
360
459
|
}, []);
|
|
361
|
-
|
|
460
|
+
const panelSurfaceProps = applySurface2(surface, resolvedTheme, {
|
|
461
|
+
className: cn4(
|
|
462
|
+
dropdown1SubMenuContentVariants(),
|
|
463
|
+
subMenuContentStyles,
|
|
464
|
+
contentWidth !== "w-48" && contentWidth,
|
|
465
|
+
className
|
|
466
|
+
)
|
|
467
|
+
});
|
|
468
|
+
const subMenuPanel = isOpen ? /* @__PURE__ */ jsx4(
|
|
362
469
|
"div",
|
|
363
470
|
{
|
|
364
|
-
ref:
|
|
365
|
-
|
|
471
|
+
ref: panelRef,
|
|
472
|
+
role: "menu",
|
|
473
|
+
"data-velocis-dropdown1-submenu": "",
|
|
474
|
+
...panelSurfaceProps,
|
|
475
|
+
style: {
|
|
476
|
+
...panelSurfaceProps.style,
|
|
477
|
+
...positionStyle
|
|
478
|
+
},
|
|
366
479
|
onMouseEnter: handleMouseEnter,
|
|
367
480
|
onMouseLeave: handleMouseLeave,
|
|
368
|
-
children:
|
|
369
|
-
/* @__PURE__ */ jsxs2(
|
|
370
|
-
"div",
|
|
371
|
-
{
|
|
372
|
-
role: "menuitem",
|
|
373
|
-
"aria-haspopup": "menu",
|
|
374
|
-
"aria-expanded": isOpen,
|
|
375
|
-
className: cn4(dropdown1SubMenuTriggerVariants(), subMenuTriggerStyles),
|
|
376
|
-
children: [
|
|
377
|
-
/* @__PURE__ */ jsx4("span", { className: "flex items-center gap-2", children: trigger }),
|
|
378
|
-
/* @__PURE__ */ jsx4(ChevronSubMenuIcon, { open: isOpen, className: cn4("transition-transform", subMenuIconStyles) })
|
|
379
|
-
]
|
|
380
|
-
}
|
|
381
|
-
),
|
|
382
|
-
isOpen && /* @__PURE__ */ jsx4(
|
|
383
|
-
"div",
|
|
384
|
-
{
|
|
385
|
-
role: "menu",
|
|
386
|
-
className: cn4(
|
|
387
|
-
dropdown1SubMenuContentVariants(),
|
|
388
|
-
subMenuContentStyles,
|
|
389
|
-
contentWidth !== "w-48" && contentWidth,
|
|
390
|
-
className
|
|
391
|
-
),
|
|
392
|
-
onMouseEnter: handleMouseEnter,
|
|
393
|
-
onMouseLeave: handleMouseLeave,
|
|
394
|
-
children: /* @__PURE__ */ jsx4("div", { className: "py-1", children })
|
|
395
|
-
}
|
|
396
|
-
)
|
|
397
|
-
]
|
|
481
|
+
children: /* @__PURE__ */ jsx4("div", { className: "py-1", children })
|
|
398
482
|
}
|
|
399
|
-
);
|
|
483
|
+
) : null;
|
|
484
|
+
return /* @__PURE__ */ jsxs2("div", { className: "relative w-full", onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [
|
|
485
|
+
/* @__PURE__ */ jsxs2(
|
|
486
|
+
"div",
|
|
487
|
+
{
|
|
488
|
+
ref: triggerRef,
|
|
489
|
+
role: "menuitem",
|
|
490
|
+
"aria-haspopup": "menu",
|
|
491
|
+
"aria-expanded": isOpen,
|
|
492
|
+
className: cn4(dropdown1SubMenuTriggerVariants(), subMenuTriggerStyles),
|
|
493
|
+
children: [
|
|
494
|
+
/* @__PURE__ */ jsx4("span", { className: "flex items-center gap-2", children: trigger }),
|
|
495
|
+
/* @__PURE__ */ jsx4(ChevronSubMenuIcon, { open: isOpen, className: cn4("transition-transform", subMenuIconStyles) })
|
|
496
|
+
]
|
|
497
|
+
}
|
|
498
|
+
),
|
|
499
|
+
isOpen && /* @__PURE__ */ jsx4(Portal2, { children: subMenuPanel })
|
|
500
|
+
] });
|
|
400
501
|
}
|
|
401
502
|
|
|
402
503
|
// src/hooks/useDropdown1.ts
|
|
403
|
-
import { useCallback as
|
|
504
|
+
import { useCallback as useCallback3, useState as useState3 } from "react";
|
|
404
505
|
function useDropdown1(options = {}) {
|
|
405
506
|
const [open, setOpen] = useState3(options.defaultOpen ?? false);
|
|
406
|
-
const onOpenChange =
|
|
507
|
+
const onOpenChange = useCallback3((next) => {
|
|
407
508
|
setOpen(next);
|
|
408
509
|
}, []);
|
|
409
510
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velocis/dropdown1",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"class-variance-authority": "^0.7.1",
|
|
26
26
|
"@velocis/core": "0.1.0",
|
|
27
|
-
"@velocis/theme": "0.
|
|
27
|
+
"@velocis/theme": "0.2.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/react": "^19.0.2",
|