@votodigital-onpeui/react 0.1.55 → 0.1.56

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.
@@ -445,6 +445,61 @@ var unlockBodyScroll = (id, enabled) => {
445
445
  document.body.style.overflow = "";
446
446
  }
447
447
  };
448
+ var FOCUSABLE_SELECTOR = [
449
+ "a[href]",
450
+ "area[href]",
451
+ "button:not([disabled])",
452
+ 'input:not([disabled]):not([type="hidden"])',
453
+ "select:not([disabled])",
454
+ "textarea:not([disabled])",
455
+ "iframe",
456
+ "object",
457
+ "embed",
458
+ '[tabindex]:not([tabindex="-1"])',
459
+ '[contenteditable="true"]'
460
+ ].join(",");
461
+ var FOCUS_GUARD_ATTRIBUTE = "data-focus-guard";
462
+ var focusGuardStyle = {
463
+ position: "absolute",
464
+ width: "1px",
465
+ height: "1px",
466
+ padding: 0,
467
+ margin: 0,
468
+ overflow: "hidden",
469
+ clip: "rect(0, 0, 0, 0)",
470
+ whiteSpace: "nowrap",
471
+ border: 0
472
+ };
473
+ var isElementVisible = (element) => {
474
+ const style = globalThis.getComputedStyle(element);
475
+ return style.visibility !== "hidden" && style.display !== "none" && element.offsetParent !== null;
476
+ };
477
+ var getFocusableElements = (wrapper, includeWrapper = true) => {
478
+ let focusable = Array.from(wrapper.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
479
+ (el) => !el.hasAttribute(FOCUS_GUARD_ATTRIBUTE) && isElementVisible(el) && el.tabIndex !== -1
480
+ );
481
+ if (includeWrapper && wrapper.tabIndex >= 0) {
482
+ focusable = [wrapper, ...focusable];
483
+ }
484
+ return focusable;
485
+ };
486
+ var focusWrapper = (wrapper, options) => {
487
+ if (wrapper.tabIndex >= 0) {
488
+ wrapper.focus(options);
489
+ return;
490
+ }
491
+ const focusable = getFocusableElements(wrapper, false);
492
+ focusable[0]?.focus(options);
493
+ };
494
+ var focusEdgeElement = (wrapper, position, options) => {
495
+ const focusable = getFocusableElements(wrapper, false);
496
+ const target = position === "last" ? focusable.at(-1) : focusable[0];
497
+ if (target) {
498
+ target.focus(options);
499
+ return;
500
+ }
501
+ focusWrapper(wrapper, options);
502
+ };
448
503
  var Modal = ({
449
504
  isOpen,
450
505
  onClose,
@@ -473,6 +528,16 @@ var Modal = ({
473
528
  const modalRef = react.useRef(null);
474
529
  const contentRef = react.useRef(null);
475
530
  const previousActiveElement = react.useRef(null);
531
+ const handleStartFocusGuard = () => {
532
+ const wrapper = modalRef.current;
533
+ if (!wrapper) return;
534
+ focusEdgeElement(wrapper, "last", { preventScroll: true });
535
+ };
536
+ const handleEndFocusGuard = () => {
537
+ const wrapper = modalRef.current;
538
+ if (!wrapper) return;
539
+ focusEdgeElement(wrapper, "first", { preventScroll: true });
540
+ };
476
541
  const [mounted, setMounted] = react.useState(false);
477
542
  const [visible, setVisible] = react.useState(false);
478
543
  const [cachedChildren, setCachedChildren] = react.useState(children);
@@ -522,52 +587,21 @@ var Modal = ({
522
587
  [10, 50, 100, 200].forEach((d) => setTimeout(resetScroll, d));
523
588
  }, [isOpen]);
524
589
  react.useEffect(() => {
525
- let focusOutWrapper = null;
526
590
  const pendingTasks = [];
527
- const isElementVisible = (element) => {
528
- const style = globalThis.getComputedStyle(element);
529
- return style.visibility !== "hidden" && style.display !== "none" && element.offsetParent !== null;
530
- };
531
- const getFocusableElements = (wrapper) => {
532
- const selector = [
533
- "a[href]",
534
- "area[href]",
535
- "button:not([disabled])",
536
- 'input:not([disabled]):not([type="hidden"])',
537
- "select:not([disabled])",
538
- "textarea:not([disabled])",
539
- "iframe",
540
- "object",
541
- "embed",
542
- '[tabindex]:not([tabindex="-1"])',
543
- '[contenteditable="true"]'
544
- ].join(",");
545
- let focusable = Array.from(
546
- wrapper.querySelectorAll(selector)
547
- ).filter((el) => isElementVisible(el) && el.tabIndex !== -1);
548
- if (wrapper.tabIndex >= 0) {
549
- focusable = [wrapper, ...focusable];
550
- }
551
- return focusable;
552
- };
553
- const handleFocusOut = (e) => {
591
+ const handleDocumentFocusIn = (e) => {
554
592
  if (!isOpen || disableFocus) return;
555
593
  const wrapper = modalRef.current;
556
- if (!wrapper) return;
557
- const relatedTarget = e.relatedTarget;
558
- if (relatedTarget && !wrapper.contains(relatedTarget)) {
559
- setTimeout(() => {
560
- const currentActive = document.activeElement;
561
- if (!currentActive || !wrapper.contains(currentActive)) {
562
- const focusable = getFocusableElements(wrapper);
563
- if (focusable.length > 0) {
564
- focusable[focusable.length - 1].focus();
565
- } else {
566
- wrapper.focus();
567
- }
568
- }
569
- }, 0);
594
+ const target = e.target;
595
+ if (!wrapper || !(target instanceof HTMLElement) || wrapper.contains(target)) {
596
+ return;
570
597
  }
598
+ requestAnimationFrame(() => {
599
+ const currentActive = document.activeElement;
600
+ if (currentActive instanceof HTMLElement && wrapper.contains(currentActive)) {
601
+ return;
602
+ }
603
+ focusWrapper(wrapper, { preventScroll: true });
604
+ });
571
605
  };
572
606
  const handleKeyDown = (e) => {
573
607
  if (e.key === "Escape" && escapeToClose && !closeDisabled) {
@@ -586,7 +620,7 @@ var Modal = ({
586
620
  if ((e.key === "ArrowUp" || e.key === "ArrowLeft") && activeIndex2 === 0) {
587
621
  e.preventDefault();
588
622
  e.stopPropagation();
589
- if (focusable.length > 1) focusable[focusable.length - 1].focus();
623
+ if (focusable.length > 1) focusable.at(-1)?.focus();
590
624
  else active.focus();
591
625
  return;
592
626
  }
@@ -610,7 +644,7 @@ var Modal = ({
610
644
  e.preventDefault();
611
645
  if (focusable.length > 0) {
612
646
  if (e.key === "ArrowUp" || e.key === "ArrowLeft")
613
- focusable[focusable.length - 1].focus();
647
+ focusable.at(-1)?.focus();
614
648
  else focusable[0].focus();
615
649
  } else {
616
650
  wrapper.focus();
@@ -625,8 +659,13 @@ var Modal = ({
625
659
  return;
626
660
  }
627
661
  const first = focusable[0];
628
- const last = focusable[focusable.length - 1];
662
+ const last = focusable.at(-1);
629
663
  const isShift = e.shiftKey;
664
+ if (!first || !last) {
665
+ e.preventDefault();
666
+ wrapper.focus();
667
+ return;
668
+ }
630
669
  if (!active || !wrapper.contains(active)) {
631
670
  e.preventDefault();
632
671
  (isShift ? last : first).focus();
@@ -649,14 +688,7 @@ var Modal = ({
649
688
  if (isOpen && !disableFocus) {
650
689
  previousActiveElement.current = document.activeElement;
651
690
  const focusInitial = (wrapper) => {
652
- if (ariaLabelledBy && document.getElementById(ariaLabelledBy)) {
653
- wrapper.focus({ preventScroll: true });
654
- return;
655
- }
656
- const focusable = getFocusableElements(wrapper);
657
- const first = focusable[0];
658
- if (first) first.focus({ preventScroll: true });
659
- else wrapper.focus();
691
+ focusWrapper(wrapper, { preventScroll: true });
660
692
  };
661
693
  const bindFocusManagement = (attempt = 0) => {
662
694
  const wrapper = modalRef.current;
@@ -668,14 +700,10 @@ var Modal = ({
668
700
  }
669
701
  return;
670
702
  }
671
- if (focusOutWrapper !== wrapper) {
672
- focusOutWrapper?.removeEventListener("focusout", handleFocusOut);
673
- wrapper.addEventListener("focusout", handleFocusOut);
674
- focusOutWrapper = wrapper;
675
- }
676
703
  focusInitial(wrapper);
677
704
  };
678
705
  document.addEventListener("keydown", handleKeyDown);
706
+ document.addEventListener("focusin", handleDocumentFocusIn, true);
679
707
  pendingTasks.push(globalThis.setTimeout(() => bindFocusManagement(), 0));
680
708
  } else if (isOpen && disableFocus) {
681
709
  document.addEventListener("keydown", handleKeyDown);
@@ -683,7 +711,7 @@ var Modal = ({
683
711
  return () => {
684
712
  pendingTasks.forEach((task) => globalThis.clearTimeout(task));
685
713
  document.removeEventListener("keydown", handleKeyDown);
686
- focusOutWrapper?.removeEventListener("focusout", handleFocusOut);
714
+ document.removeEventListener("focusin", handleDocumentFocusIn, true);
687
715
  if (!disableFocus && !disableFocusRestore && previousActiveElement.current) {
688
716
  previousActiveElement.current.focus();
689
717
  }
@@ -743,6 +771,16 @@ var Modal = ({
743
771
  "aria-describedby": props["aria-describedby"],
744
772
  "aria-label": props["aria-label"],
745
773
  children: [
774
+ /* @__PURE__ */ jsxRuntime.jsx(
775
+ "span",
776
+ {
777
+ tabIndex: disableFocus ? -1 : 0,
778
+ "aria-hidden": "true",
779
+ ...{ [FOCUS_GUARD_ATTRIBUTE]: "start" },
780
+ style: focusGuardStyle,
781
+ onFocus: handleStartFocusGuard
782
+ }
783
+ ),
746
784
  /* @__PURE__ */ jsxRuntime.jsx("div", { ref: contentRef, className: contentClass, children: isOpen ? children : cachedChildren }),
747
785
  closeButton && /* @__PURE__ */ jsxRuntime.jsx(
748
786
  "button",
@@ -753,6 +791,16 @@ var Modal = ({
753
791
  type: "button",
754
792
  children: /* @__PURE__ */ jsxRuntime.jsx(IconCloseRadius, { "aria-hidden": "true", className: "w-full h-full" })
755
793
  }
794
+ ),
795
+ /* @__PURE__ */ jsxRuntime.jsx(
796
+ "span",
797
+ {
798
+ tabIndex: disableFocus ? -1 : 0,
799
+ "aria-hidden": "true",
800
+ ...{ [FOCUS_GUARD_ATTRIBUTE]: "end" },
801
+ style: focusGuardStyle,
802
+ onFocus: handleEndFocusGuard
803
+ }
756
804
  )
757
805
  ]
758
806
  }