@velocis/dropdown1 0.3.0 → 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 +136 -46
- package/dist/index.d.cts +12 -4
- package/dist/index.d.ts +12 -4
- package/dist/index.js +139 -49
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -59,7 +59,9 @@ var DROPDOWN1_ITEM_DEFAULTS = {
|
|
|
59
59
|
};
|
|
60
60
|
var DROPDOWN1_SUBMENU_DEFAULTS = {
|
|
61
61
|
contentWidth: "w-48",
|
|
62
|
-
hoverDelayMs: 100
|
|
62
|
+
hoverDelayMs: 100,
|
|
63
|
+
placement: "auto",
|
|
64
|
+
subMenuGap: 8
|
|
63
65
|
};
|
|
64
66
|
|
|
65
67
|
// src/Dropdown1.variants.ts
|
|
@@ -93,7 +95,7 @@ var dropdown1SubMenuTriggerVariants = (0, import_class_variance_authority.cva)(
|
|
|
93
95
|
"flex w-full cursor-default items-center justify-between px-4 py-2 text-sm transition-colors"
|
|
94
96
|
);
|
|
95
97
|
var dropdown1SubMenuContentVariants = (0, import_class_variance_authority.cva)(
|
|
96
|
-
"
|
|
98
|
+
"fixed w-48 rounded-velocis-md z-[10000]"
|
|
97
99
|
);
|
|
98
100
|
|
|
99
101
|
// src/context/Dropdown1Context.tsx
|
|
@@ -131,7 +133,7 @@ function resolveDropdown1Styles(overrides) {
|
|
|
131
133
|
content,
|
|
132
134
|
item: overrides?.item ?? dropdown1Styles.item,
|
|
133
135
|
subMenuTrigger: overrides?.subMenuTrigger ?? dropdown1Styles.subMenuTrigger,
|
|
134
|
-
subMenuContent: overrides?.subMenuContent ??
|
|
136
|
+
subMenuContent: overrides?.subMenuContent ?? dropdown1Styles.subMenuContent,
|
|
135
137
|
subMenuIcon: overrides?.subMenuIcon ?? dropdown1Styles.subMenuIcon
|
|
136
138
|
};
|
|
137
139
|
}
|
|
@@ -216,10 +218,18 @@ function Dropdown1Root({
|
|
|
216
218
|
const { resolvedTheme } = (0, import_theme.useTheme)();
|
|
217
219
|
const { open, setOpen } = useControllableOpen(controlledOpen, defaultOpen, onOpenChange);
|
|
218
220
|
const styles = resolveDropdown1Styles(stylesProp);
|
|
219
|
-
const
|
|
221
|
+
const contentRef = (0, import_react2.useRef)(null);
|
|
220
222
|
const buttonRef = (0, import_react2.useRef)(null);
|
|
221
223
|
const [positionStyle, setPositionStyle] = (0, import_react2.useState)({});
|
|
222
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
|
+
}, []);
|
|
223
233
|
const updatePosition = (0, import_react2.useCallback)(() => {
|
|
224
234
|
if (!buttonRef.current) return;
|
|
225
235
|
const rect = buttonRef.current.getBoundingClientRect();
|
|
@@ -241,7 +251,7 @@ function Dropdown1Root({
|
|
|
241
251
|
(0, import_react2.useEffect)(() => {
|
|
242
252
|
if (!open) return;
|
|
243
253
|
const handleClickOutside = (event) => {
|
|
244
|
-
if (
|
|
254
|
+
if (buttonRef.current && !buttonRef.current.contains(event.target) && !isInsideDropdownPanel(event.target)) {
|
|
245
255
|
setOpen(false);
|
|
246
256
|
}
|
|
247
257
|
};
|
|
@@ -253,7 +263,7 @@ function Dropdown1Root({
|
|
|
253
263
|
const handleScroll = (event) => {
|
|
254
264
|
if (!closeOnScroll) return;
|
|
255
265
|
const target = event.target;
|
|
256
|
-
if (
|
|
266
|
+
if (target && !isInsideDropdownPanel(target) && !buttonRef.current?.contains(target)) {
|
|
257
267
|
setOpen(false);
|
|
258
268
|
}
|
|
259
269
|
};
|
|
@@ -265,7 +275,7 @@ function Dropdown1Root({
|
|
|
265
275
|
document.removeEventListener("keydown", handleKeyDown);
|
|
266
276
|
document.removeEventListener("scroll", handleScroll, true);
|
|
267
277
|
};
|
|
268
|
-
}, [closeOnScroll, open, setOpen]);
|
|
278
|
+
}, [closeOnScroll, isInsideDropdownPanel, open, setOpen]);
|
|
269
279
|
const contentSurfaceProps = (0, import_theme.applySurface)(surface, resolvedTheme, {
|
|
270
280
|
className: (0, import_core3.cn)(
|
|
271
281
|
dropdown1ContentVariants(),
|
|
@@ -275,10 +285,10 @@ function Dropdown1Root({
|
|
|
275
285
|
contentClassName
|
|
276
286
|
)
|
|
277
287
|
});
|
|
278
|
-
const dropdownContent = open ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dropdown1Context.Provider, { value: { closeDropdown, styles }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
288
|
+
const dropdownContent = open ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dropdown1Context.Provider, { value: { closeDropdown, styles, contentRef }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
279
289
|
"div",
|
|
280
290
|
{
|
|
281
|
-
ref:
|
|
291
|
+
ref: contentRef,
|
|
282
292
|
role: "menu",
|
|
283
293
|
"data-testid": contentTestId,
|
|
284
294
|
dir: direction,
|
|
@@ -369,28 +379,101 @@ function Dropdown1Item({
|
|
|
369
379
|
}
|
|
370
380
|
|
|
371
381
|
// src/components/Dropdown1SubMenu.tsx
|
|
372
|
-
var
|
|
382
|
+
var import_core6 = require("@velocis/core");
|
|
383
|
+
var import_theme2 = require("@velocis/theme");
|
|
373
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
|
|
374
432
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
375
433
|
function Dropdown1SubMenu({
|
|
376
434
|
trigger,
|
|
377
435
|
children,
|
|
436
|
+
placement = DROPDOWN1_SUBMENU_DEFAULTS.placement,
|
|
378
437
|
contentWidth = DROPDOWN1_SUBMENU_DEFAULTS.contentWidth,
|
|
379
438
|
styles: stylesProp,
|
|
439
|
+
surface,
|
|
380
440
|
className
|
|
381
441
|
}) {
|
|
442
|
+
const direction = (0, import_core6.useDirection)();
|
|
443
|
+
const { resolvedTheme } = (0, import_theme2.useTheme)();
|
|
382
444
|
const context = useDropdown1Context();
|
|
383
445
|
const subMenuTriggerStyles = stylesProp?.subMenuTrigger ?? context?.styles.subMenuTrigger;
|
|
384
446
|
const subMenuContentStyles = stylesProp?.subMenuContent ?? context?.styles.subMenuContent;
|
|
385
447
|
const subMenuIconStyles = stylesProp?.subMenuIcon ?? context?.styles.subMenuIcon;
|
|
386
448
|
const [isOpen, setIsOpen] = (0, import_react3.useState)(false);
|
|
449
|
+
const [positionStyle, setPositionStyle] = (0, import_react3.useState)({});
|
|
387
450
|
const timeoutRef = (0, import_react3.useRef)(null);
|
|
388
|
-
const
|
|
389
|
-
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 = () => {
|
|
390
470
|
if (timeoutRef.current) {
|
|
391
471
|
clearTimeout(timeoutRef.current);
|
|
392
472
|
timeoutRef.current = null;
|
|
393
473
|
}
|
|
474
|
+
};
|
|
475
|
+
const handleMouseEnter = () => {
|
|
476
|
+
clearCloseTimeout();
|
|
394
477
|
setIsOpen(true);
|
|
395
478
|
};
|
|
396
479
|
const handleMouseLeave = () => {
|
|
@@ -398,6 +481,11 @@ function Dropdown1SubMenu({
|
|
|
398
481
|
setIsOpen(false);
|
|
399
482
|
}, DROPDOWN1_SUBMENU_DEFAULTS.hoverDelayMs);
|
|
400
483
|
};
|
|
484
|
+
(0, import_react3.useEffect)(() => {
|
|
485
|
+
if (isOpen) {
|
|
486
|
+
updatePosition();
|
|
487
|
+
}
|
|
488
|
+
}, [isOpen, updatePosition]);
|
|
401
489
|
(0, import_react3.useEffect)(() => {
|
|
402
490
|
return () => {
|
|
403
491
|
if (timeoutRef.current) {
|
|
@@ -405,45 +493,47 @@ function Dropdown1SubMenu({
|
|
|
405
493
|
}
|
|
406
494
|
};
|
|
407
495
|
}, []);
|
|
408
|
-
|
|
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)(
|
|
409
505
|
"div",
|
|
410
506
|
{
|
|
411
|
-
ref:
|
|
412
|
-
|
|
507
|
+
ref: panelRef,
|
|
508
|
+
role: "menu",
|
|
509
|
+
"data-velocis-dropdown1-submenu": "",
|
|
510
|
+
...panelSurfaceProps,
|
|
511
|
+
style: {
|
|
512
|
+
...panelSurfaceProps.style,
|
|
513
|
+
...positionStyle
|
|
514
|
+
},
|
|
413
515
|
onMouseEnter: handleMouseEnter,
|
|
414
516
|
onMouseLeave: handleMouseLeave,
|
|
415
|
-
children:
|
|
416
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
417
|
-
"div",
|
|
418
|
-
{
|
|
419
|
-
role: "menuitem",
|
|
420
|
-
"aria-haspopup": "menu",
|
|
421
|
-
"aria-expanded": isOpen,
|
|
422
|
-
className: (0, import_core5.cn)(dropdown1SubMenuTriggerVariants(), subMenuTriggerStyles),
|
|
423
|
-
children: [
|
|
424
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "flex items-center gap-2", children: trigger }),
|
|
425
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChevronSubMenuIcon, { open: isOpen, className: (0, import_core5.cn)("transition-transform", subMenuIconStyles) })
|
|
426
|
-
]
|
|
427
|
-
}
|
|
428
|
-
),
|
|
429
|
-
isOpen && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
430
|
-
"div",
|
|
431
|
-
{
|
|
432
|
-
role: "menu",
|
|
433
|
-
className: (0, import_core5.cn)(
|
|
434
|
-
dropdown1SubMenuContentVariants(),
|
|
435
|
-
subMenuContentStyles,
|
|
436
|
-
contentWidth !== "w-48" && contentWidth,
|
|
437
|
-
className
|
|
438
|
-
),
|
|
439
|
-
onMouseEnter: handleMouseEnter,
|
|
440
|
-
onMouseLeave: handleMouseLeave,
|
|
441
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "py-1", children })
|
|
442
|
-
}
|
|
443
|
-
)
|
|
444
|
-
]
|
|
517
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "py-1", children })
|
|
445
518
|
}
|
|
446
|
-
);
|
|
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
|
+
] });
|
|
447
537
|
}
|
|
448
538
|
|
|
449
539
|
// src/hooks/useDropdown1.ts
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { ReactNode, RefObject } from 'react';
|
|
3
3
|
import { VelocisSurfaceConfig } from '@velocis/theme';
|
|
4
4
|
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
5
5
|
|
|
@@ -14,7 +14,7 @@ type Dropdown1Styles = {
|
|
|
14
14
|
item?: string;
|
|
15
15
|
/** Sub-menu row — text + hover bg/text */
|
|
16
16
|
subMenuTrigger?: string;
|
|
17
|
-
/** Sub-menu panel —
|
|
17
|
+
/** Sub-menu panel — independent default from `content` */
|
|
18
18
|
subMenuContent?: string;
|
|
19
19
|
/** Chevron on sub-menu row */
|
|
20
20
|
subMenuIcon?: string;
|
|
@@ -63,12 +63,17 @@ type Dropdown1ItemProps = {
|
|
|
63
63
|
className?: string;
|
|
64
64
|
testId?: string;
|
|
65
65
|
};
|
|
66
|
+
type Dropdown1SubMenuPlacement = 'auto' | 'left' | 'right' | 'center';
|
|
66
67
|
type Dropdown1SubMenuProps = {
|
|
67
68
|
trigger: ReactNode;
|
|
68
69
|
children: ReactNode;
|
|
70
|
+
/** Where the sub-panel opens relative to the main menu panel */
|
|
71
|
+
placement?: Dropdown1SubMenuPlacement;
|
|
69
72
|
contentWidth?: string;
|
|
70
73
|
/** Override sub-menu colors (defaults to root styles) */
|
|
71
74
|
styles?: Pick<Dropdown1Styles, 'subMenuTrigger' | 'subMenuContent' | 'subMenuIcon'>;
|
|
75
|
+
/** Independent background/tokens — separate from root `surface` */
|
|
76
|
+
surface?: VelocisSurfaceConfig;
|
|
72
77
|
className?: string;
|
|
73
78
|
};
|
|
74
79
|
|
|
@@ -76,7 +81,7 @@ declare function Dropdown1Root({ trigger, children, open: controlledOpen, defaul
|
|
|
76
81
|
|
|
77
82
|
declare function Dropdown1Item({ children, onClick, closeOnSelect, styles: stylesProp, className, testId, }: Dropdown1ItemProps): react.JSX.Element;
|
|
78
83
|
|
|
79
|
-
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;
|
|
80
85
|
|
|
81
86
|
type UseDropdown1Options = {
|
|
82
87
|
defaultOpen?: boolean;
|
|
@@ -91,6 +96,7 @@ declare function useDropdown1(options?: UseDropdown1Options): UseDropdown1Result
|
|
|
91
96
|
type Dropdown1ContextValue = {
|
|
92
97
|
closeDropdown: () => void;
|
|
93
98
|
styles: ResolvedDropdown1Styles;
|
|
99
|
+
contentRef: RefObject<HTMLDivElement | null>;
|
|
94
100
|
};
|
|
95
101
|
declare const Dropdown1Context: react.Context<Dropdown1ContextValue | null>;
|
|
96
102
|
declare function useDropdown1Context(): Dropdown1ContextValue | null;
|
|
@@ -108,6 +114,8 @@ declare const DROPDOWN1_ITEM_DEFAULTS: {
|
|
|
108
114
|
declare const DROPDOWN1_SUBMENU_DEFAULTS: {
|
|
109
115
|
readonly contentWidth: "w-48";
|
|
110
116
|
readonly hoverDelayMs: 100;
|
|
117
|
+
readonly placement: "auto";
|
|
118
|
+
readonly subMenuGap: 8;
|
|
111
119
|
};
|
|
112
120
|
|
|
113
121
|
/** Layout/structure only — colors come from `dropdown1Styles` + `styles` prop */
|
|
@@ -125,4 +133,4 @@ declare const Dropdown1: typeof Dropdown1Root & {
|
|
|
125
133
|
SubMenu: typeof Dropdown1SubMenu;
|
|
126
134
|
};
|
|
127
135
|
|
|
128
|
-
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,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { ReactNode, RefObject } from 'react';
|
|
3
3
|
import { VelocisSurfaceConfig } from '@velocis/theme';
|
|
4
4
|
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
5
5
|
|
|
@@ -14,7 +14,7 @@ type Dropdown1Styles = {
|
|
|
14
14
|
item?: string;
|
|
15
15
|
/** Sub-menu row — text + hover bg/text */
|
|
16
16
|
subMenuTrigger?: string;
|
|
17
|
-
/** Sub-menu panel —
|
|
17
|
+
/** Sub-menu panel — independent default from `content` */
|
|
18
18
|
subMenuContent?: string;
|
|
19
19
|
/** Chevron on sub-menu row */
|
|
20
20
|
subMenuIcon?: string;
|
|
@@ -63,12 +63,17 @@ type Dropdown1ItemProps = {
|
|
|
63
63
|
className?: string;
|
|
64
64
|
testId?: string;
|
|
65
65
|
};
|
|
66
|
+
type Dropdown1SubMenuPlacement = 'auto' | 'left' | 'right' | 'center';
|
|
66
67
|
type Dropdown1SubMenuProps = {
|
|
67
68
|
trigger: ReactNode;
|
|
68
69
|
children: ReactNode;
|
|
70
|
+
/** Where the sub-panel opens relative to the main menu panel */
|
|
71
|
+
placement?: Dropdown1SubMenuPlacement;
|
|
69
72
|
contentWidth?: string;
|
|
70
73
|
/** Override sub-menu colors (defaults to root styles) */
|
|
71
74
|
styles?: Pick<Dropdown1Styles, 'subMenuTrigger' | 'subMenuContent' | 'subMenuIcon'>;
|
|
75
|
+
/** Independent background/tokens — separate from root `surface` */
|
|
76
|
+
surface?: VelocisSurfaceConfig;
|
|
72
77
|
className?: string;
|
|
73
78
|
};
|
|
74
79
|
|
|
@@ -76,7 +81,7 @@ declare function Dropdown1Root({ trigger, children, open: controlledOpen, defaul
|
|
|
76
81
|
|
|
77
82
|
declare function Dropdown1Item({ children, onClick, closeOnSelect, styles: stylesProp, className, testId, }: Dropdown1ItemProps): react.JSX.Element;
|
|
78
83
|
|
|
79
|
-
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;
|
|
80
85
|
|
|
81
86
|
type UseDropdown1Options = {
|
|
82
87
|
defaultOpen?: boolean;
|
|
@@ -91,6 +96,7 @@ declare function useDropdown1(options?: UseDropdown1Options): UseDropdown1Result
|
|
|
91
96
|
type Dropdown1ContextValue = {
|
|
92
97
|
closeDropdown: () => void;
|
|
93
98
|
styles: ResolvedDropdown1Styles;
|
|
99
|
+
contentRef: RefObject<HTMLDivElement | null>;
|
|
94
100
|
};
|
|
95
101
|
declare const Dropdown1Context: react.Context<Dropdown1ContextValue | null>;
|
|
96
102
|
declare function useDropdown1Context(): Dropdown1ContextValue | null;
|
|
@@ -108,6 +114,8 @@ declare const DROPDOWN1_ITEM_DEFAULTS: {
|
|
|
108
114
|
declare const DROPDOWN1_SUBMENU_DEFAULTS: {
|
|
109
115
|
readonly contentWidth: "w-48";
|
|
110
116
|
readonly hoverDelayMs: 100;
|
|
117
|
+
readonly placement: "auto";
|
|
118
|
+
readonly subMenuGap: 8;
|
|
111
119
|
};
|
|
112
120
|
|
|
113
121
|
/** Layout/structure only — colors come from `dropdown1Styles` + `styles` prop */
|
|
@@ -125,4 +133,4 @@ declare const Dropdown1: typeof Dropdown1Root & {
|
|
|
125
133
|
SubMenu: typeof Dropdown1SubMenu;
|
|
126
134
|
};
|
|
127
135
|
|
|
128
|
-
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
|
@@ -23,7 +23,9 @@ var DROPDOWN1_ITEM_DEFAULTS = {
|
|
|
23
23
|
};
|
|
24
24
|
var DROPDOWN1_SUBMENU_DEFAULTS = {
|
|
25
25
|
contentWidth: "w-48",
|
|
26
|
-
hoverDelayMs: 100
|
|
26
|
+
hoverDelayMs: 100,
|
|
27
|
+
placement: "auto",
|
|
28
|
+
subMenuGap: 8
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
// src/Dropdown1.variants.ts
|
|
@@ -57,7 +59,7 @@ var dropdown1SubMenuTriggerVariants = cva(
|
|
|
57
59
|
"flex w-full cursor-default items-center justify-between px-4 py-2 text-sm transition-colors"
|
|
58
60
|
);
|
|
59
61
|
var dropdown1SubMenuContentVariants = cva(
|
|
60
|
-
"
|
|
62
|
+
"fixed w-48 rounded-velocis-md z-[10000]"
|
|
61
63
|
);
|
|
62
64
|
|
|
63
65
|
// src/context/Dropdown1Context.tsx
|
|
@@ -95,7 +97,7 @@ function resolveDropdown1Styles(overrides) {
|
|
|
95
97
|
content,
|
|
96
98
|
item: overrides?.item ?? dropdown1Styles.item,
|
|
97
99
|
subMenuTrigger: overrides?.subMenuTrigger ?? dropdown1Styles.subMenuTrigger,
|
|
98
|
-
subMenuContent: overrides?.subMenuContent ??
|
|
100
|
+
subMenuContent: overrides?.subMenuContent ?? dropdown1Styles.subMenuContent,
|
|
99
101
|
subMenuIcon: overrides?.subMenuIcon ?? dropdown1Styles.subMenuIcon
|
|
100
102
|
};
|
|
101
103
|
}
|
|
@@ -180,10 +182,18 @@ function Dropdown1Root({
|
|
|
180
182
|
const { resolvedTheme } = useTheme();
|
|
181
183
|
const { open, setOpen } = useControllableOpen(controlledOpen, defaultOpen, onOpenChange);
|
|
182
184
|
const styles = resolveDropdown1Styles(stylesProp);
|
|
183
|
-
const
|
|
185
|
+
const contentRef = useRef(null);
|
|
184
186
|
const buttonRef = useRef(null);
|
|
185
187
|
const [positionStyle, setPositionStyle] = useState({});
|
|
186
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
|
+
}, []);
|
|
187
197
|
const updatePosition = useCallback(() => {
|
|
188
198
|
if (!buttonRef.current) return;
|
|
189
199
|
const rect = buttonRef.current.getBoundingClientRect();
|
|
@@ -205,7 +215,7 @@ function Dropdown1Root({
|
|
|
205
215
|
useEffect(() => {
|
|
206
216
|
if (!open) return;
|
|
207
217
|
const handleClickOutside = (event) => {
|
|
208
|
-
if (
|
|
218
|
+
if (buttonRef.current && !buttonRef.current.contains(event.target) && !isInsideDropdownPanel(event.target)) {
|
|
209
219
|
setOpen(false);
|
|
210
220
|
}
|
|
211
221
|
};
|
|
@@ -217,7 +227,7 @@ function Dropdown1Root({
|
|
|
217
227
|
const handleScroll = (event) => {
|
|
218
228
|
if (!closeOnScroll) return;
|
|
219
229
|
const target = event.target;
|
|
220
|
-
if (
|
|
230
|
+
if (target && !isInsideDropdownPanel(target) && !buttonRef.current?.contains(target)) {
|
|
221
231
|
setOpen(false);
|
|
222
232
|
}
|
|
223
233
|
};
|
|
@@ -229,7 +239,7 @@ function Dropdown1Root({
|
|
|
229
239
|
document.removeEventListener("keydown", handleKeyDown);
|
|
230
240
|
document.removeEventListener("scroll", handleScroll, true);
|
|
231
241
|
};
|
|
232
|
-
}, [closeOnScroll, open, setOpen]);
|
|
242
|
+
}, [closeOnScroll, isInsideDropdownPanel, open, setOpen]);
|
|
233
243
|
const contentSurfaceProps = applySurface(surface, resolvedTheme, {
|
|
234
244
|
className: cn2(
|
|
235
245
|
dropdown1ContentVariants(),
|
|
@@ -239,10 +249,10 @@ function Dropdown1Root({
|
|
|
239
249
|
contentClassName
|
|
240
250
|
)
|
|
241
251
|
});
|
|
242
|
-
const dropdownContent = open ? /* @__PURE__ */ jsx2(Dropdown1Context.Provider, { value: { closeDropdown, styles }, children: /* @__PURE__ */ jsx2(
|
|
252
|
+
const dropdownContent = open ? /* @__PURE__ */ jsx2(Dropdown1Context.Provider, { value: { closeDropdown, styles, contentRef }, children: /* @__PURE__ */ jsx2(
|
|
243
253
|
"div",
|
|
244
254
|
{
|
|
245
|
-
ref:
|
|
255
|
+
ref: contentRef,
|
|
246
256
|
role: "menu",
|
|
247
257
|
"data-testid": contentTestId,
|
|
248
258
|
dir: direction,
|
|
@@ -333,28 +343,101 @@ function Dropdown1Item({
|
|
|
333
343
|
}
|
|
334
344
|
|
|
335
345
|
// src/components/Dropdown1SubMenu.tsx
|
|
336
|
-
import { cn as cn4 } from "@velocis/core";
|
|
337
|
-
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
|
|
338
396
|
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
339
397
|
function Dropdown1SubMenu({
|
|
340
398
|
trigger,
|
|
341
399
|
children,
|
|
400
|
+
placement = DROPDOWN1_SUBMENU_DEFAULTS.placement,
|
|
342
401
|
contentWidth = DROPDOWN1_SUBMENU_DEFAULTS.contentWidth,
|
|
343
402
|
styles: stylesProp,
|
|
403
|
+
surface,
|
|
344
404
|
className
|
|
345
405
|
}) {
|
|
406
|
+
const direction = useDirection2();
|
|
407
|
+
const { resolvedTheme } = useTheme2();
|
|
346
408
|
const context = useDropdown1Context();
|
|
347
409
|
const subMenuTriggerStyles = stylesProp?.subMenuTrigger ?? context?.styles.subMenuTrigger;
|
|
348
410
|
const subMenuContentStyles = stylesProp?.subMenuContent ?? context?.styles.subMenuContent;
|
|
349
411
|
const subMenuIconStyles = stylesProp?.subMenuIcon ?? context?.styles.subMenuIcon;
|
|
350
412
|
const [isOpen, setIsOpen] = useState2(false);
|
|
413
|
+
const [positionStyle, setPositionStyle] = useState2({});
|
|
351
414
|
const timeoutRef = useRef2(null);
|
|
352
|
-
const
|
|
353
|
-
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 = () => {
|
|
354
434
|
if (timeoutRef.current) {
|
|
355
435
|
clearTimeout(timeoutRef.current);
|
|
356
436
|
timeoutRef.current = null;
|
|
357
437
|
}
|
|
438
|
+
};
|
|
439
|
+
const handleMouseEnter = () => {
|
|
440
|
+
clearCloseTimeout();
|
|
358
441
|
setIsOpen(true);
|
|
359
442
|
};
|
|
360
443
|
const handleMouseLeave = () => {
|
|
@@ -362,6 +445,11 @@ function Dropdown1SubMenu({
|
|
|
362
445
|
setIsOpen(false);
|
|
363
446
|
}, DROPDOWN1_SUBMENU_DEFAULTS.hoverDelayMs);
|
|
364
447
|
};
|
|
448
|
+
useEffect2(() => {
|
|
449
|
+
if (isOpen) {
|
|
450
|
+
updatePosition();
|
|
451
|
+
}
|
|
452
|
+
}, [isOpen, updatePosition]);
|
|
365
453
|
useEffect2(() => {
|
|
366
454
|
return () => {
|
|
367
455
|
if (timeoutRef.current) {
|
|
@@ -369,52 +457,54 @@ function Dropdown1SubMenu({
|
|
|
369
457
|
}
|
|
370
458
|
};
|
|
371
459
|
}, []);
|
|
372
|
-
|
|
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(
|
|
373
469
|
"div",
|
|
374
470
|
{
|
|
375
|
-
ref:
|
|
376
|
-
|
|
471
|
+
ref: panelRef,
|
|
472
|
+
role: "menu",
|
|
473
|
+
"data-velocis-dropdown1-submenu": "",
|
|
474
|
+
...panelSurfaceProps,
|
|
475
|
+
style: {
|
|
476
|
+
...panelSurfaceProps.style,
|
|
477
|
+
...positionStyle
|
|
478
|
+
},
|
|
377
479
|
onMouseEnter: handleMouseEnter,
|
|
378
480
|
onMouseLeave: handleMouseLeave,
|
|
379
|
-
children:
|
|
380
|
-
/* @__PURE__ */ jsxs2(
|
|
381
|
-
"div",
|
|
382
|
-
{
|
|
383
|
-
role: "menuitem",
|
|
384
|
-
"aria-haspopup": "menu",
|
|
385
|
-
"aria-expanded": isOpen,
|
|
386
|
-
className: cn4(dropdown1SubMenuTriggerVariants(), subMenuTriggerStyles),
|
|
387
|
-
children: [
|
|
388
|
-
/* @__PURE__ */ jsx4("span", { className: "flex items-center gap-2", children: trigger }),
|
|
389
|
-
/* @__PURE__ */ jsx4(ChevronSubMenuIcon, { open: isOpen, className: cn4("transition-transform", subMenuIconStyles) })
|
|
390
|
-
]
|
|
391
|
-
}
|
|
392
|
-
),
|
|
393
|
-
isOpen && /* @__PURE__ */ jsx4(
|
|
394
|
-
"div",
|
|
395
|
-
{
|
|
396
|
-
role: "menu",
|
|
397
|
-
className: cn4(
|
|
398
|
-
dropdown1SubMenuContentVariants(),
|
|
399
|
-
subMenuContentStyles,
|
|
400
|
-
contentWidth !== "w-48" && contentWidth,
|
|
401
|
-
className
|
|
402
|
-
),
|
|
403
|
-
onMouseEnter: handleMouseEnter,
|
|
404
|
-
onMouseLeave: handleMouseLeave,
|
|
405
|
-
children: /* @__PURE__ */ jsx4("div", { className: "py-1", children })
|
|
406
|
-
}
|
|
407
|
-
)
|
|
408
|
-
]
|
|
481
|
+
children: /* @__PURE__ */ jsx4("div", { className: "py-1", children })
|
|
409
482
|
}
|
|
410
|
-
);
|
|
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
|
+
] });
|
|
411
501
|
}
|
|
412
502
|
|
|
413
503
|
// src/hooks/useDropdown1.ts
|
|
414
|
-
import { useCallback as
|
|
504
|
+
import { useCallback as useCallback3, useState as useState3 } from "react";
|
|
415
505
|
function useDropdown1(options = {}) {
|
|
416
506
|
const [open, setOpen] = useState3(options.defaultOpen ?? false);
|
|
417
|
-
const onOpenChange =
|
|
507
|
+
const onOpenChange = useCallback3((next) => {
|
|
418
508
|
setOpen(next);
|
|
419
509
|
}, []);
|
|
420
510
|
return {
|