@vitus-labs/elements 2.0.0-alpha.27 → 2.0.0-alpha.29

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/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Provider, alignContent, extendCss, makeItResponsive, value } from "@vitus-labs/unistyle";
2
2
  import { config, context, isEmpty, omit, pick, render, throttle } from "@vitus-labs/core";
3
- import { Children, createContext, forwardRef, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { Children, createContext, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { isFragment } from "react-is";
6
6
  import { createPortal } from "react-dom";
@@ -150,14 +150,12 @@ const childFixCSS = `
150
150
  const parentFixCSS = `
151
151
  flex-direction: column;
152
152
  `;
153
- const parentFixBlockCSS = `
154
- width: 100%;
155
- `;
156
153
  const fullHeightCSS = `
157
154
  height: 100%;
158
155
  `;
159
156
  const blockCSS = `
160
157
  align-self: stretch;
158
+ width: 100%;
161
159
  `;
162
160
  const childFixPosition = (isBlock) => `display: ${isBlock ? "flex" : "inline-flex"};`;
163
161
  const styles$1 = ({ theme: t, css }) => css`
@@ -170,9 +168,9 @@ const styles$1 = ({ theme: t, css }) => css`
170
168
  })};
171
169
 
172
170
  ${t.block && blockCSS};
171
+ ${t.alignY === "block" && t.block && fullHeightCSS};
173
172
 
174
173
  ${!t.childFix && childFixPosition(t.block)};
175
- ${t.parentFix && t.block && parentFixBlockCSS};
176
174
  ${t.parentFix && parentFixCSS};
177
175
 
178
176
  ${t.extraStyles && extendCss(t.extraStyles)};
@@ -216,13 +214,12 @@ const isWebFixNeeded = (tag) => {
216
214
  //#region src/helpers/Wrapper/component.tsx
217
215
  /**
218
216
  * Wrapper component that serves as the outermost styled container for Element.
219
- * Uses forwardRef for ref forwarding to the underlying DOM node. On web, it
220
- * detects button/fieldset/legend tags and applies a two-layer flex fix
217
+ * On web, it detects button/fieldset/legend tags and applies a two-layer flex fix
221
218
  * (parent + child Styled) because these HTML elements do not natively
222
219
  * support `display: flex` consistently across browsers.
223
220
  */
224
221
  const DEV_PROPS = IS_DEVELOPMENT ? { "data-vl-element": "Element" } : {};
225
- const Component$8 = forwardRef(({ children, tag, block, extendCss, direction, alignX, alignY, equalCols, isInline, ...props }, ref) => {
222
+ const Component$8 = ({ children, ref, tag, block, extendCss, direction, alignX, alignY, equalCols, isInline, ...props }) => {
226
223
  const COMMON_PROPS = {
227
224
  ...props,
228
225
  ...DEV_PROPS,
@@ -278,7 +275,7 @@ const Component$8 = forwardRef(({ children, tag, block, extendCss, direction, al
278
275
  children
279
276
  })
280
277
  });
281
- });
278
+ };
282
279
 
283
280
  //#endregion
284
281
  //#region src/helpers/Wrapper/index.ts
@@ -388,7 +385,7 @@ const defaultDirection = "inline";
388
385
  const defaultContentDirection = "rows";
389
386
  const defaultAlignX = "left";
390
387
  const defaultAlignY = "center";
391
- const Component$7 = forwardRef(({ innerRef, tag, label, content, children, beforeContent, afterContent, equalBeforeAfter, block, equalCols, gap, direction, alignX = defaultAlignX, alignY = defaultAlignY, css, contentCss, beforeContentCss, afterContentCss, contentDirection = defaultContentDirection, contentAlignX = defaultAlignX, contentAlignY = defaultAlignY, beforeContentDirection = defaultDirection, beforeContentAlignX = defaultAlignX, beforeContentAlignY = defaultAlignY, afterContentDirection = defaultDirection, afterContentAlignX = defaultAlignX, afterContentAlignY = defaultAlignY, ...props }, ref) => {
388
+ const Component$7 = ({ innerRef, ref, tag, label, content, children, beforeContent, afterContent, equalBeforeAfter, block, equalCols, gap, direction, alignX = defaultAlignX, alignY = defaultAlignY, css, contentCss, beforeContentCss, afterContentCss, contentDirection = defaultContentDirection, contentAlignX = defaultAlignX, contentAlignY = defaultAlignY, beforeContentDirection = defaultDirection, beforeContentAlignX = defaultAlignX, beforeContentAlignY = defaultAlignY, afterContentDirection = defaultDirection, afterContentAlignX = defaultAlignX, afterContentAlignY = defaultAlignY, ...props }) => {
392
389
  const shouldBeEmpty = !!props.dangerouslySetInnerHTML || getShouldBeEmpty(tag);
393
390
  const isSimpleElement = !beforeContent && !afterContent;
394
391
  const CHILDREN = children ?? content ?? label;
@@ -427,7 +424,13 @@ const Component$7 = forwardRef(({ innerRef, tag, label, content, children, befor
427
424
  }, [externalRef]);
428
425
  useLayoutEffect(() => {
429
426
  if (!equalBeforeAfter || !beforeContent || !afterContent) return;
430
- if (equalizeRef.current) equalize(equalizeRef.current, direction);
427
+ const node = equalizeRef.current;
428
+ if (!node) return;
429
+ equalize(node, direction);
430
+ if (typeof ResizeObserver === "undefined") return;
431
+ const observer = new ResizeObserver(() => equalize(node, direction));
432
+ observer.observe(node);
433
+ return () => observer.disconnect();
431
434
  }, [
432
435
  equalBeforeAfter,
433
436
  beforeContent,
@@ -490,7 +493,7 @@ const Component$7 = forwardRef(({ innerRef, tag, label, content, children, befor
490
493
  })
491
494
  ]
492
495
  });
493
- });
496
+ };
494
497
  const name$5 = `${PKG_NAME}/Element`;
495
498
  Component$7.displayName = name$5;
496
499
  Component$7.pkgName = PKG_NAME;
@@ -510,27 +513,6 @@ var Element_default = Component$7;
510
513
  * wrapped with `wrapComponent`. Children always take priority over the
511
514
  * component+data prop pattern.
512
515
  */
513
- const classifyData = (data) => {
514
- const items = data.filter((item) => item != null && !(typeof item === "object" && isEmpty(item)));
515
- if (items.length === 0) return null;
516
- let isSimple = true;
517
- let isComplex = true;
518
- for (const item of items) if (typeof item === "string" || typeof item === "number") isComplex = false;
519
- else if (typeof item === "object") isSimple = false;
520
- else {
521
- isSimple = false;
522
- isComplex = false;
523
- }
524
- if (isSimple) return {
525
- type: "simple",
526
- data: items
527
- };
528
- if (isComplex) return {
529
- type: "complex",
530
- data: items
531
- };
532
- return null;
533
- };
534
516
  const RESERVED_PROPS = [
535
517
  "children",
536
518
  "component",
@@ -541,7 +523,7 @@ const RESERVED_PROPS = [
541
523
  "itemProps",
542
524
  "wrapProps"
543
525
  ];
544
- const attachItemProps = ({ i, length }) => {
526
+ const buildExtendedProps = (i, length) => {
545
527
  const position = i + 1;
546
528
  return {
547
529
  index: i,
@@ -552,106 +534,96 @@ const attachItemProps = ({ i, length }) => {
552
534
  position
553
535
  };
554
536
  };
555
- const Component$6 = (props) => {
556
- const { itemKey, valueName, children, component, data, wrapComponent: Wrapper, wrapProps, itemProps } = props;
557
- const injectItemProps = useMemo(() => typeof itemProps === "function" ? itemProps : () => itemProps, [itemProps]);
558
- const injectWrapItemProps = useMemo(() => typeof wrapProps === "function" ? wrapProps : () => wrapProps, [wrapProps]);
559
- const getKey = useCallback((item, index) => {
560
- if (typeof itemKey === "function") return itemKey(item, index);
561
- return index;
562
- }, [itemKey]);
563
- const renderChild = (child, total = 1, i = 0) => {
564
- if (!itemProps && !Wrapper) return child;
565
- const extendedProps = attachItemProps({
566
- i,
567
- length: total
568
- });
569
- const finalItemProps = itemProps ? injectItemProps({}, extendedProps) : {};
570
- if (Wrapper) return /* @__PURE__ */ jsx(Wrapper, {
571
- ...wrapProps ? injectWrapItemProps({}, extendedProps) : {},
572
- children: render(child, finalItemProps)
573
- }, i);
574
- return render(child, {
575
- key: i,
576
- ...finalItemProps
577
- });
578
- };
579
- const renderChildren = () => {
580
- if (!children) return null;
581
- if (Array.isArray(children)) return Children.map(children, (item, i) => renderChild(item, children.length, i));
582
- if (isFragment(children)) {
583
- const fragmentChildren = children.props.children;
584
- const childrenLength = fragmentChildren.length;
585
- return fragmentChildren.map((item, i) => renderChild(item, childrenLength, i));
586
- }
587
- return renderChild(children);
588
- };
589
- const renderSimpleArray = (data) => {
590
- const { length } = data;
591
- if (length === 0) return null;
592
- return data.map((item, i) => {
593
- const key = getKey(item, i);
594
- const keyName = valueName ?? "children";
595
- const extendedProps = attachItemProps({
596
- i,
597
- length
598
- });
599
- const finalItemProps = {
600
- ...itemProps ? injectItemProps({ [keyName]: item }, extendedProps) : {},
601
- [keyName]: item
602
- };
603
- if (Wrapper) return /* @__PURE__ */ jsx(Wrapper, {
604
- ...wrapProps ? injectWrapItemProps({ [keyName]: item }, extendedProps) : {},
605
- children: render(component, finalItemProps)
606
- }, key);
607
- return render(component, {
608
- key,
609
- ...finalItemProps
610
- });
611
- });
612
- };
613
- const getObjectKey = (item, index) => {
614
- if (!itemKey) return item.key ?? item.id ?? item.itemId ?? index;
615
- if (typeof itemKey === "function") return itemKey(item, index);
616
- if (typeof itemKey === "string") return item[itemKey];
617
- return index;
537
+ const resolveCallback = (cb, source, ext) => {
538
+ if (!cb) return {};
539
+ return typeof cb === "function" ? cb(source, ext) : cb;
540
+ };
541
+ const renderSpec = (spec, ext, itemProps, wrapProps, Wrapper) => {
542
+ const finalItemProps = {
543
+ ...resolveCallback(itemProps, spec.source, ext),
544
+ ...spec.base
618
545
  };
619
- const renderComplexArray = (data) => {
620
- const { length } = data;
621
- if (length === 0) return null;
622
- return data.map((item, i) => {
623
- const { component: itemComponent, ...restItem } = item;
624
- const renderItem = itemComponent ?? component;
625
- const key = getObjectKey(restItem, i);
626
- const extendedProps = attachItemProps({
627
- i,
628
- length
629
- });
630
- const finalItemProps = {
631
- ...itemProps ? injectItemProps(item, extendedProps) : {},
632
- ...restItem
633
- };
634
- if (Wrapper && !itemComponent) return /* @__PURE__ */ jsx(Wrapper, {
635
- ...wrapProps ? injectWrapItemProps(item, extendedProps) : {},
636
- children: render(renderItem, finalItemProps)
637
- }, key);
638
- return render(renderItem, {
639
- key,
640
- ...finalItemProps
641
- });
642
- });
546
+ if (Wrapper && !spec.skipWrap) return /* @__PURE__ */ jsx(Wrapper, {
547
+ ...resolveCallback(wrapProps, spec.source, ext),
548
+ children: spec.isNode ? render(spec.target, finalItemProps) : render(spec.target, finalItemProps)
549
+ }, spec.key);
550
+ const propsWithKey = {
551
+ key: spec.key,
552
+ ...finalItemProps
643
553
  };
644
- const renderItems = () => {
645
- if (children) return renderChildren();
646
- if (component && Array.isArray(data)) {
647
- const classified = classifyData(data);
648
- if (!classified) return null;
649
- if (classified.type === "simple") return renderSimpleArray(classified.data);
650
- return renderComplexArray(classified.data);
651
- }
652
- return null;
554
+ return spec.isNode ? render(spec.target, propsWithKey) : render(spec.target, propsWithKey);
555
+ };
556
+ /** Normalize children (single, array, or fragment) into an array of nodes. */
557
+ const flattenChildren = (children) => {
558
+ if (Array.isArray(children)) return children;
559
+ if (isFragment(children)) return children.props.children;
560
+ return [children];
561
+ };
562
+ /** Drop nullish entries and empty objects (matches legacy behavior). */
563
+ const filterValidItems = (data) => data.filter((item) => item != null && !(typeof item === "object" && isEmpty(item)));
564
+ /** Determine if the array is uniformly simple (string/number) or complex (object). Mixed → null. */
565
+ const detectKind = (items) => {
566
+ let kind = null;
567
+ for (const item of items) {
568
+ const t = typeof item === "string" || typeof item === "number" ? "simple" : typeof item === "object" ? "complex" : null;
569
+ if (t === null) return null;
570
+ if (kind === null) kind = t;
571
+ else if (kind !== t) return null;
572
+ }
573
+ return kind;
574
+ };
575
+ const objectKey = (item, index, itemKey) => {
576
+ if (!itemKey) return item.key ?? item.id ?? item.itemId ?? index;
577
+ if (typeof itemKey === "function") return itemKey(item, index);
578
+ if (typeof itemKey === "string") return item[itemKey] ?? index;
579
+ return index;
580
+ };
581
+ const buildChildrenSpecs = (children) => flattenChildren(children).map((node, i) => ({
582
+ key: i,
583
+ target: node,
584
+ source: {},
585
+ base: {},
586
+ isNode: true,
587
+ skipWrap: false
588
+ }));
589
+ const buildSimpleSpecs = (items, component, valueName, itemKey) => {
590
+ const keyName = valueName ?? "children";
591
+ return items.map((value, i) => ({
592
+ key: typeof itemKey === "function" ? itemKey(value, i) : i,
593
+ target: component,
594
+ source: { [keyName]: value },
595
+ base: { [keyName]: value },
596
+ isNode: false,
597
+ skipWrap: false
598
+ }));
599
+ };
600
+ const buildObjectSpecs = (items, component, itemKey) => items.map((item, i) => {
601
+ const { component: itemComponent, ...rest } = item;
602
+ return {
603
+ key: objectKey(rest, i, itemKey),
604
+ target: itemComponent ?? component,
605
+ source: item,
606
+ base: rest,
607
+ isNode: false,
608
+ skipWrap: Boolean(itemComponent)
653
609
  };
654
- return renderItems();
610
+ });
611
+ const buildDataSpecs = (data, component, valueName, itemKey) => {
612
+ const items = filterValidItems(data);
613
+ if (items.length === 0) return null;
614
+ const kind = detectKind(items);
615
+ if (!kind) return null;
616
+ return kind === "simple" ? buildSimpleSpecs(items, component, valueName, itemKey) : buildObjectSpecs(items, component, itemKey);
617
+ };
618
+ const Component$6 = ({ itemKey, valueName, children, component, data, wrapComponent: Wrapper, wrapProps, itemProps }) => {
619
+ let specs = null;
620
+ if (children) {
621
+ if (!Array.isArray(children) && !isFragment(children) && !itemProps && !Wrapper) return children;
622
+ specs = buildChildrenSpecs(children);
623
+ } else if (component && Array.isArray(data)) specs = buildDataSpecs(data, component, valueName, itemKey);
624
+ if (!specs || specs.length === 0) return null;
625
+ const total = specs.length;
626
+ return Children.toArray(specs.map((spec, i) => renderSpec(spec, buildExtendedProps(i, total), itemProps, wrapProps, Wrapper)));
655
627
  };
656
628
  var component_default = Object.assign(memo(Component$6), {
657
629
  isIterator: true,
@@ -671,7 +643,7 @@ var Iterator_default = component_default;
671
643
  * is wrapped in an Element that receives all non-iterator props (e.g.,
672
644
  * layout, alignment, css), allowing the list to be styled as a single block.
673
645
  */
674
- const Component$5 = forwardRef(({ rootElement = false, ...props }, ref) => {
646
+ const Component$5 = ({ rootElement = false, ref, ...props }) => {
675
647
  const renderedList = /* @__PURE__ */ jsx(Iterator_default, { ...pick(props, Iterator_default.RESERVED_PROPS) });
676
648
  if (!rootElement) return renderedList;
677
649
  return /* @__PURE__ */ jsx(Element_default, {
@@ -679,7 +651,7 @@ const Component$5 = forwardRef(({ rootElement = false, ...props }, ref) => {
679
651
  ...omit(props, Iterator_default.RESERVED_PROPS),
680
652
  children: renderedList
681
653
  });
682
- });
654
+ };
683
655
  const name$4 = `${PKG_NAME}/List`;
684
656
  Component$5.displayName = name$4;
685
657
  Component$5.pkgName = PKG_NAME;
@@ -748,14 +720,12 @@ const Component = ({ children, blocked, setBlocked, setUnblocked }) => {
748
720
  };
749
721
 
750
722
  //#endregion
751
- //#region src/Overlay/useOverlay.tsx
723
+ //#region src/Overlay/positionMath.ts
752
724
  /**
753
- * Core hook powering the Overlay component. Manages open/close state, DOM
754
- * event listeners (click, hover, scroll, resize, ESC key), and dynamic
755
- * positioning of overlay content relative to its trigger. Supports dropdown,
756
- * tooltip, popover, and modal types with automatic edge-of-viewport flipping.
757
- * Event handlers are throttled for performance, and nested overlay blocking
758
- * is coordinated through the overlay context.
725
+ * Pure positioning math for the Overlay component. No React, no DOM mutations.
726
+ * Given the current trigger and content rects (plus alignment options), computes
727
+ * the final viewport-relative position and the alignment that was actually used
728
+ * (which may differ from the requested alignment when an edge would be clipped).
759
729
  */
760
730
  const sel = (cond, a, b) => cond ? a : b;
761
731
  const devWarn = (msg) => {
@@ -925,24 +895,192 @@ const processVisibilityEvent = (e, active, openOn, closeOn, isTrigger, isContent
925
895
  else if (closeOn === "clickOnTrigger" && isTrigger(e)) hideContent();
926
896
  else if (closeOn === "clickOutsideContent" && !isContent(e)) hideContent();
927
897
  };
928
- const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type = "dropdown", position = "fixed", align = "bottom", alignX = "left", alignY = "bottom", offsetX = 0, offsetY = 0, throttleDelay = 200, parentContainer, closeOnEsc = true, disabled, onOpen, onClose } = {}) => {
898
+
899
+ //#endregion
900
+ //#region src/Overlay/useEscapeKey.ts
901
+ /** Closes the overlay on Escape keypress when `enabled` and `active`. */
902
+ const useEscapeKey = (enabled, active, blocked, hide) => {
903
+ useEffect(() => {
904
+ if (!enabled || !active || blocked) return void 0;
905
+ const onKey = (e) => {
906
+ if (e.key === "Escape") hide();
907
+ };
908
+ window.addEventListener("keydown", onKey);
909
+ return () => window.removeEventListener("keydown", onKey);
910
+ }, [
911
+ enabled,
912
+ active,
913
+ blocked,
914
+ hide
915
+ ]);
916
+ };
917
+
918
+ //#endregion
919
+ //#region src/Overlay/useHoverListeners.ts
920
+ /**
921
+ * Hover-based open/close. Uses mouseenter/mouseleave on trigger + content
922
+ * (instead of window-level mousemove) and a configurable delay to bridge
923
+ * the gap between trigger and content elements without flicker.
924
+ */
925
+ const useHoverListeners = ({ triggerRef, contentRef, isContentLoaded, active, blocked, disabled, openOn, closeOn, hoverDelay, showContent, hideContent }) => {
926
+ const hoverTimeoutRef = useRef(null);
927
+ useEffect(() => {
928
+ if (blocked || disabled || !(openOn === "hover" || closeOn === "hover")) return void 0;
929
+ const trigger = triggerRef.current;
930
+ const content = contentRef.current;
931
+ const clearHoverTimeout = () => {
932
+ if (hoverTimeoutRef.current != null) {
933
+ clearTimeout(hoverTimeoutRef.current);
934
+ hoverTimeoutRef.current = null;
935
+ }
936
+ };
937
+ const scheduleHide = () => {
938
+ clearHoverTimeout();
939
+ hoverTimeoutRef.current = setTimeout(hideContent, hoverDelay);
940
+ };
941
+ const onTriggerEnter = () => {
942
+ clearHoverTimeout();
943
+ if (openOn === "hover" && !active) showContent();
944
+ };
945
+ const onTriggerLeave = () => {
946
+ if (closeOn === "hover" && active) scheduleHide();
947
+ };
948
+ const onContentEnter = () => {
949
+ clearHoverTimeout();
950
+ };
951
+ const onContentLeave = () => {
952
+ if (closeOn === "hover" && active) scheduleHide();
953
+ };
954
+ if (trigger) {
955
+ trigger.addEventListener("mouseenter", onTriggerEnter);
956
+ trigger.addEventListener("mouseleave", onTriggerLeave);
957
+ }
958
+ if (content) {
959
+ content.addEventListener("mouseenter", onContentEnter);
960
+ content.addEventListener("mouseleave", onContentLeave);
961
+ }
962
+ return () => {
963
+ clearHoverTimeout();
964
+ if (trigger) {
965
+ trigger.removeEventListener("mouseenter", onTriggerEnter);
966
+ trigger.removeEventListener("mouseleave", onTriggerLeave);
967
+ }
968
+ if (content) {
969
+ content.removeEventListener("mouseenter", onContentEnter);
970
+ content.removeEventListener("mouseleave", onContentLeave);
971
+ }
972
+ };
973
+ }, [
974
+ active,
975
+ isContentLoaded,
976
+ blocked,
977
+ disabled,
978
+ openOn,
979
+ closeOn,
980
+ hoverDelay,
981
+ showContent,
982
+ hideContent
983
+ ]);
984
+ };
985
+
986
+ //#endregion
987
+ //#region src/Overlay/useScrollReposition.ts
988
+ let modalOverflowCount = 0;
989
+ /**
990
+ * Window-level scroll/resize listeners that reposition active overlays and
991
+ * re-evaluate close-on-scroll behavior. Also manages the body overflow lock
992
+ * for modal overlays (refcounted across nested modals).
993
+ */
994
+ const useWindowReposition = (active, type, handleContentPosition, handleVisibility) => {
995
+ useEffect(() => {
996
+ if (!active) return void 0;
997
+ const shouldSetOverflow = type === "modal";
998
+ const onScroll = (e) => {
999
+ handleContentPosition();
1000
+ handleVisibility(e);
1001
+ };
1002
+ if (shouldSetOverflow) {
1003
+ modalOverflowCount++;
1004
+ if (modalOverflowCount === 1) document.body.style.overflow = "hidden";
1005
+ }
1006
+ window.addEventListener("resize", handleContentPosition);
1007
+ window.addEventListener("scroll", onScroll, { passive: true });
1008
+ return () => {
1009
+ handleContentPosition.cancel();
1010
+ handleVisibility.cancel();
1011
+ if (shouldSetOverflow) {
1012
+ modalOverflowCount--;
1013
+ if (modalOverflowCount === 0) document.body.style.overflow = "";
1014
+ }
1015
+ window.removeEventListener("resize", handleContentPosition);
1016
+ window.removeEventListener("scroll", onScroll);
1017
+ };
1018
+ }, [
1019
+ active,
1020
+ type,
1021
+ handleContentPosition,
1022
+ handleVisibility
1023
+ ]);
1024
+ };
1025
+ /**
1026
+ * Same as `useWindowReposition` but for a custom scrollable ancestor.
1027
+ * Locks the parent's overflow while the overlay is active (unless hover-driven,
1028
+ * which expects the parent to keep scrolling).
1029
+ */
1030
+ const useParentContainerReposition = (active, parentContainer, closeOn, handleContentPosition, handleVisibility) => {
1031
+ useEffect(() => {
1032
+ if (!active || !parentContainer) return void 0;
1033
+ if (closeOn !== "hover") parentContainer.style.overflow = "hidden";
1034
+ const onScroll = (e) => {
1035
+ handleContentPosition();
1036
+ handleVisibility(e);
1037
+ };
1038
+ parentContainer.addEventListener("scroll", onScroll, { passive: true });
1039
+ return () => {
1040
+ parentContainer.style.overflow = "";
1041
+ parentContainer.removeEventListener("scroll", onScroll);
1042
+ };
1043
+ }, [
1044
+ active,
1045
+ parentContainer,
1046
+ closeOn,
1047
+ handleContentPosition,
1048
+ handleVisibility
1049
+ ]);
1050
+ };
1051
+ const useScrollReposition = ({ active, type, parentContainer, closeOn, handleContentPosition, handleVisibility }) => {
1052
+ useWindowReposition(active, type, handleContentPosition, handleVisibility);
1053
+ useParentContainerReposition(active, parentContainer, closeOn, handleContentPosition, handleVisibility);
1054
+ };
1055
+
1056
+ //#endregion
1057
+ //#region src/Overlay/useOverlay.tsx
1058
+ /**
1059
+ * Core hook powering the Overlay component. Manages open/close state, DOM
1060
+ * event listeners (click, hover, scroll, resize, ESC key), and dynamic
1061
+ * positioning of overlay content relative to its trigger. Supports dropdown,
1062
+ * tooltip, popover, and modal types with automatic edge-of-viewport flipping.
1063
+ *
1064
+ * Pure positioning math lives in `./positionMath`. Event-listener concerns
1065
+ * live in dedicated hooks: `./useEscapeKey`, `./useHoverListeners`,
1066
+ * `./useScrollReposition`.
1067
+ */
1068
+ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type = "dropdown", position = "fixed", align = "bottom", alignX = "left", alignY = "bottom", offsetX = 0, offsetY = 0, throttleDelay = 200, parentContainer, closeOnEsc = true, hoverDelay = 100, disabled, onOpen, onClose } = {}) => {
929
1069
  const { rootSize } = useContext(context);
930
1070
  const ctx = useOverlayContext();
931
1071
  const [isContentLoaded, setContentLoaded] = useState(false);
932
1072
  const [innerAlignX, setInnerAlignX] = useState(alignX);
933
1073
  const [innerAlignY, setInnerAlignY] = useState(alignY);
934
- const [blocked, handleBlocked] = useState(false);
935
- const [active, handleActive] = useState(isOpen);
1074
+ const [blockedCount, setBlockedCount] = useState(0);
1075
+ const blocked = blockedCount > 0;
1076
+ const [active, setActive] = useState(isOpen);
936
1077
  const triggerRef = useRef(null);
937
1078
  const contentRef = useRef(null);
938
- const setBlocked = useCallback(() => handleBlocked(true), []);
939
- const setUnblocked = useCallback(() => handleBlocked(false), []);
940
- const showContent = useCallback(() => {
941
- handleActive(true);
942
- }, []);
943
- const hideContent = useCallback(() => {
944
- handleActive(false);
945
- }, []);
1079
+ const prevFocusRef = useRef(null);
1080
+ const setBlocked = useCallback(() => setBlockedCount((c) => c + 1), []);
1081
+ const setUnblocked = useCallback(() => setBlockedCount((c) => Math.max(0, c - 1)), []);
1082
+ const showContent = useCallback(() => setActive(true), []);
1083
+ const hideContent = useCallback(() => setActive(false), []);
946
1084
  const getAncestorOffset = useCallback(() => {
947
1085
  if (position !== "absolute" || !contentRef.current) return {
948
1086
  top: 0,
@@ -1011,7 +1149,7 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1011
1149
  const latestHandleVisibility = useRef(handleVisibilityByEventType);
1012
1150
  latestHandleVisibility.current = handleVisibilityByEventType;
1013
1151
  const handleContentPosition = useMemo(() => throttle(() => latestSetContentPosition.current(), throttleDelay), [throttleDelay]);
1014
- const handleClick = handleVisibilityByEventType;
1152
+ const handleClick = useCallback((e) => latestHandleVisibility.current(e), []);
1015
1153
  const handleVisibility = useMemo(() => throttle((e) => latestHandleVisibility.current(e), throttleDelay), [throttleDelay]);
1016
1154
  useEffect(() => {
1017
1155
  setInnerAlignX(alignX);
@@ -1058,60 +1196,30 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1058
1196
  onOpen
1059
1197
  ]);
1060
1198
  useEffect(() => {
1061
- if (!closeOnEsc || !active || blocked) return void 0;
1062
- const handleEscKey = (e) => {
1063
- if (e.key === "Escape") hideContent();
1064
- };
1065
- window.addEventListener("keydown", handleEscKey);
1066
- return () => {
1067
- window.removeEventListener("keydown", handleEscKey);
1068
- };
1199
+ if (type !== "modal") return;
1200
+ if (active && isContentLoaded && contentRef.current) {
1201
+ prevFocusRef.current = document.activeElement;
1202
+ if (contentRef.current.tabIndex < 0) contentRef.current.tabIndex = -1;
1203
+ contentRef.current.focus();
1204
+ }
1205
+ if (!active && prevFocusRef.current) {
1206
+ prevFocusRef.current.focus();
1207
+ prevFocusRef.current = null;
1208
+ }
1069
1209
  }, [
1070
1210
  active,
1071
- blocked,
1072
- closeOnEsc,
1073
- hideContent
1211
+ isContentLoaded,
1212
+ type
1074
1213
  ]);
1075
- useEffect(() => {
1076
- if (!active) return void 0;
1077
- const shouldSetOverflow = type === "modal";
1078
- const onScroll = (e) => {
1079
- handleContentPosition();
1080
- handleVisibility(e);
1081
- };
1082
- if (shouldSetOverflow) document.body.style.overflow = "hidden";
1083
- window.addEventListener("resize", handleContentPosition);
1084
- window.addEventListener("scroll", onScroll, { passive: true });
1085
- return () => {
1086
- if (shouldSetOverflow) document.body.style.overflow = "";
1087
- window.removeEventListener("resize", handleContentPosition);
1088
- window.removeEventListener("scroll", onScroll);
1089
- };
1090
- }, [
1214
+ useEscapeKey(closeOnEsc, active, blocked, hideContent);
1215
+ useScrollReposition({
1091
1216
  active,
1092
1217
  type,
1093
- handleVisibility,
1094
- handleContentPosition
1095
- ]);
1096
- useEffect(() => {
1097
- if (!active || !parentContainer) return void 0;
1098
- if (closeOn !== "hover") parentContainer.style.overflow = "hidden";
1099
- const onScroll = (e) => {
1100
- handleContentPosition();
1101
- handleVisibility(e);
1102
- };
1103
- parentContainer.addEventListener("scroll", onScroll, { passive: true });
1104
- return () => {
1105
- parentContainer.style.overflow = "";
1106
- parentContainer.removeEventListener("scroll", onScroll);
1107
- };
1108
- }, [
1109
- active,
1110
1218
  parentContainer,
1111
1219
  closeOn,
1112
1220
  handleContentPosition,
1113
1221
  handleVisibility
1114
- ]);
1222
+ });
1115
1223
  useEffect(() => {
1116
1224
  if (blocked || disabled) return void 0;
1117
1225
  if (openOn === "click" || [
@@ -1119,9 +1227,7 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1119
1227
  "clickOnTrigger",
1120
1228
  "clickOutsideContent"
1121
1229
  ].includes(closeOn)) window.addEventListener("click", handleClick);
1122
- return () => {
1123
- window.removeEventListener("click", handleClick);
1124
- };
1230
+ return () => window.removeEventListener("click", handleClick);
1125
1231
  }, [
1126
1232
  openOn,
1127
1233
  closeOn,
@@ -1129,70 +1235,24 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1129
1235
  disabled,
1130
1236
  handleClick
1131
1237
  ]);
1132
- const hoverTimeoutRef = useRef(null);
1133
- useEffect(() => {
1134
- if (blocked || disabled || !(openOn === "hover" || closeOn === "hover")) return void 0;
1135
- const trigger = triggerRef.current;
1136
- const content = contentRef.current;
1137
- const clearHoverTimeout = () => {
1138
- if (hoverTimeoutRef.current != null) {
1139
- clearTimeout(hoverTimeoutRef.current);
1140
- hoverTimeoutRef.current = null;
1141
- }
1142
- };
1143
- const scheduleHide = () => {
1144
- clearHoverTimeout();
1145
- hoverTimeoutRef.current = setTimeout(hideContent, 100);
1146
- };
1147
- const onTriggerEnter = () => {
1148
- clearHoverTimeout();
1149
- if (openOn === "hover" && !active) showContent();
1150
- };
1151
- const onTriggerLeave = () => {
1152
- if (closeOn === "hover" && active) scheduleHide();
1153
- };
1154
- const onContentEnter = () => {
1155
- clearHoverTimeout();
1156
- };
1157
- const onContentLeave = () => {
1158
- if (closeOn === "hover" && active) scheduleHide();
1159
- };
1160
- if (trigger) {
1161
- trigger.addEventListener("mouseenter", onTriggerEnter);
1162
- trigger.addEventListener("mouseleave", onTriggerLeave);
1163
- }
1164
- if (content) {
1165
- content.addEventListener("mouseenter", onContentEnter);
1166
- content.addEventListener("mouseleave", onContentLeave);
1167
- }
1168
- return () => {
1169
- clearHoverTimeout();
1170
- if (trigger) {
1171
- trigger.removeEventListener("mouseenter", onTriggerEnter);
1172
- trigger.removeEventListener("mouseleave", onTriggerLeave);
1173
- }
1174
- if (content) {
1175
- content.removeEventListener("mouseenter", onContentEnter);
1176
- content.removeEventListener("mouseleave", onContentLeave);
1177
- }
1178
- };
1179
- }, [
1180
- active,
1238
+ useHoverListeners({
1239
+ triggerRef,
1240
+ contentRef,
1181
1241
  isContentLoaded,
1242
+ active,
1182
1243
  blocked,
1183
1244
  disabled,
1184
1245
  openOn,
1185
1246
  closeOn,
1247
+ hoverDelay,
1186
1248
  showContent,
1187
1249
  hideContent
1188
- ]);
1250
+ });
1189
1251
  return {
1190
1252
  triggerRef,
1191
1253
  contentRef: useCallback((node) => {
1192
- if (node) {
1193
- contentRef.current = node;
1194
- setContentLoaded(true);
1195
- }
1254
+ contentRef.current = node;
1255
+ setContentLoaded(!!node);
1196
1256
  }, []),
1197
1257
  active,
1198
1258
  align,
@@ -1219,11 +1279,22 @@ const useOverlay = ({ isOpen = false, openOn = "click", closeOn = "click", type
1219
1279
  const IS_BROWSER = typeof window !== "undefined";
1220
1280
  const Component$3 = ({ children, trigger, DOMLocation, triggerRefName = "ref", contentRefName = "ref", ...props }) => {
1221
1281
  const { active, triggerRef, contentRef, showContent, hideContent, align, alignX, alignY, Provider, ...ctx } = useOverlay(props);
1222
- const { openOn, closeOn } = props;
1282
+ const { openOn, closeOn, type } = props;
1283
+ const contentId = useId();
1223
1284
  const passHandlers = useMemo(() => openOn === "manual" || closeOn === "manual" || closeOn === "clickOutsideContent", [openOn, closeOn]);
1285
+ const ariaHasPopup = useMemo(() => {
1286
+ switch (type) {
1287
+ case "modal": return "dialog";
1288
+ case "tooltip": return "true";
1289
+ default: return "menu";
1290
+ }
1291
+ }, [type]);
1224
1292
  return /* @__PURE__ */ jsxs(Fragment, { children: [render(trigger, {
1225
1293
  [triggerRefName]: triggerRef,
1226
1294
  active,
1295
+ "aria-expanded": active,
1296
+ "aria-haspopup": ariaHasPopup,
1297
+ "aria-controls": active ? contentId : void 0,
1227
1298
  ...passHandlers ? {
1228
1299
  showContent,
1229
1300
  hideContent
@@ -1234,6 +1305,9 @@ const Component$3 = ({ children, trigger, DOMLocation, triggerRefName = "ref", c
1234
1305
  ...ctx,
1235
1306
  children: render(children, {
1236
1307
  [contentRefName]: contentRef,
1308
+ id: contentId,
1309
+ role: type === "modal" ? "dialog" : void 0,
1310
+ "aria-modal": type === "modal" ? true : void 0,
1237
1311
  active,
1238
1312
  align,
1239
1313
  alignX,
@@ -1268,9 +1342,11 @@ const styles = ({ css, theme: t }) => css`
1268
1342
  ${t.extraStyles && extendCss(t.extraStyles)};
1269
1343
  `;
1270
1344
  var styled_default = styled(textComponent)`
1271
- color: inherit;
1272
- font-weight: inherit;
1273
- line-height: 1;
1345
+ ${css`
1346
+ color: inherit;
1347
+ font-weight: inherit;
1348
+ line-height: 1;
1349
+ `};
1274
1350
 
1275
1351
  ${makeItResponsive({
1276
1352
  key: "$text",
@@ -1282,7 +1358,7 @@ var styled_default = styled(textComponent)`
1282
1358
 
1283
1359
  //#endregion
1284
1360
  //#region src/Text/component.tsx
1285
- const Component$2 = forwardRef(({ paragraph, label, children, tag, css, ...props }, ref) => {
1361
+ const Component$2 = ({ paragraph, label, children, tag, css, ref, ...props }) => {
1286
1362
  const renderContent = (as = void 0) => /* @__PURE__ */ jsx(styled_default, {
1287
1363
  ref,
1288
1364
  as,
@@ -1294,7 +1370,7 @@ const Component$2 = forwardRef(({ paragraph, label, children, tag, css, ...props
1294
1370
  if (paragraph) finalTag = "p";
1295
1371
  else finalTag = tag;
1296
1372
  return renderContent(finalTag);
1297
- });
1373
+ };
1298
1374
  const name$1 = `${PKG_NAME}/Text`;
1299
1375
  Component$2.displayName = name$1;
1300
1376
  Component$2.pkgName = PKG_NAME;