@kenos-ui/react-datepicker 0.3.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.d.cts CHANGED
@@ -74,6 +74,8 @@ interface DatePickerConfig {
74
74
  disabled?: boolean | ((date: Date) => boolean);
75
75
  readOnly: boolean;
76
76
  closeOnSelect: boolean;
77
+ /** Opt-in focus trap + aria-modal. Default: false (popup-policy). */
78
+ modal: boolean;
77
79
  }
78
80
 
79
81
  type RootProps = DatePickerRootProps & {
package/dist/index.d.ts CHANGED
@@ -74,6 +74,8 @@ interface DatePickerConfig {
74
74
  disabled?: boolean | ((date: Date) => boolean);
75
75
  readOnly: boolean;
76
76
  closeOnSelect: boolean;
77
+ /** Opt-in focus trap + aria-modal. Default: false (popup-policy). */
78
+ modal: boolean;
77
79
  }
78
80
 
79
81
  type RootProps = DatePickerRootProps & {
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import React, { createContext, useContext, useMemo, useRef, useState, useEffect,
2
2
  import { jsx, jsxs } from 'react/jsx-runtime';
3
3
  import { useTimescape } from 'timescape/react';
4
4
  import { createPortal } from 'react-dom';
5
+ import { restoreFocus, useClickOutside, useEscapeKey, useFocusTrap } from '@kenos-ui/utils';
5
6
  import { useFloating, autoUpdate, offset, flip, shift } from '@floating-ui/react-dom';
6
7
 
7
8
  var __defProp = Object.defineProperty;
@@ -412,6 +413,7 @@ function resolveConfig(props) {
412
413
  mode: props.mode ?? "single",
413
414
  locale: props.locale ?? (typeof navigator !== "undefined" ? navigator.language : "en-US"),
414
415
  readOnly: props.readOnly ?? false,
416
+ modal: props.modal ?? false,
415
417
  closeOnSelect: props.closeOnSelect ?? (props.mode !== "range" && props.mode !== "multiple"),
416
418
  ...props.weekStartsOn !== void 0 && { weekStartsOn: props.weekStartsOn },
417
419
  ...props.minDate !== void 0 && { minDate: props.minDate },
@@ -437,7 +439,7 @@ function resolveInitialValue(props) {
437
439
  }
438
440
  function useDatePicker(props) {
439
441
  const uid = useId();
440
- const { mode, locale, weekStartsOn, minDate, maxDate, disabled, readOnly, closeOnSelect } = props;
442
+ const { mode, locale, weekStartsOn, minDate, maxDate, disabled, readOnly, closeOnSelect, modal } = props;
441
443
  const config = useMemo(
442
444
  () => resolveConfig({
443
445
  mode,
@@ -447,9 +449,10 @@ function useDatePicker(props) {
447
449
  maxDate,
448
450
  disabled,
449
451
  readOnly,
450
- closeOnSelect
452
+ closeOnSelect,
453
+ modal
451
454
  }),
452
- [mode, locale, weekStartsOn, minDate, maxDate, disabled, readOnly, closeOnSelect]
455
+ [mode, locale, weekStartsOn, minDate, maxDate, disabled, readOnly, closeOnSelect, modal]
453
456
  );
454
457
  const initialValue = resolveInitialValue(props);
455
458
  const [state, dispatch] = useReducer(
@@ -651,6 +654,7 @@ function Segments({
651
654
  }
652
655
  if (e.key === "Escape") {
653
656
  e.preventDefault();
657
+ e.stopPropagation();
654
658
  if (state.open) dispatch({ type: "CLOSE" });
655
659
  }
656
660
  }
@@ -801,76 +805,6 @@ function Trigger({ children, onClick, disabled, ...props }) {
801
805
  }
802
806
  );
803
807
  }
804
- function useClickOutside(refs, handler, enabled = true) {
805
- const refsRef = useRef(refs);
806
- refsRef.current = refs;
807
- useEffect(() => {
808
- if (!enabled) return;
809
- function onPointerDown(e) {
810
- const target = e.target;
811
- if (refsRef.current.every((r) => !r.current?.contains(target))) {
812
- handler();
813
- }
814
- }
815
- document.addEventListener("pointerdown", onPointerDown, true);
816
- return () => document.removeEventListener("pointerdown", onPointerDown, true);
817
- }, [enabled, handler]);
818
- }
819
-
820
- // src/utils/aria.ts
821
- function getFocusableElements(container) {
822
- const selector = [
823
- "a[href]",
824
- "button:not([disabled])",
825
- "input:not([disabled])",
826
- "select:not([disabled])",
827
- "textarea:not([disabled])",
828
- '[tabindex]:not([tabindex="-1"])'
829
- ].join(", ");
830
- return Array.from(container.querySelectorAll(selector)).filter(
831
- (el) => !el.closest("[hidden]") && el.offsetParent !== null
832
- );
833
- }
834
-
835
- // src/date-picker/use-focus-trap.ts
836
- function useFocusTrap(containerRef, enabled = true) {
837
- useEffect(() => {
838
- if (!enabled || !containerRef.current) return;
839
- const container = containerRef.current;
840
- function onKeyDown(e) {
841
- if (e.key !== "Tab") return;
842
- const focusable = getFocusableElements(container);
843
- if (!focusable.length) return;
844
- const first = focusable[0];
845
- const last = focusable[focusable.length - 1];
846
- if (e.shiftKey) {
847
- if (document.activeElement === first) {
848
- e.preventDefault();
849
- last.focus();
850
- }
851
- } else {
852
- if (document.activeElement === last) {
853
- e.preventDefault();
854
- first.focus();
855
- }
856
- }
857
- }
858
- const observer = new MutationObserver(() => {
859
- const active = document.activeElement;
860
- if (active?.getAttribute("role") === "spinbutton") return;
861
- if (!container.contains(active)) {
862
- const firstFocusable = getFocusableElements(container)[0];
863
- firstFocusable?.focus();
864
- }
865
- });
866
- observer.observe(container, { childList: true, subtree: true });
867
- container.addEventListener("keydown", onKeyDown);
868
- return () => {
869
- container.removeEventListener("keydown", onKeyDown);
870
- observer.disconnect();
871
- };
872
- }, [enabled, containerRef]);
873
- }
874
808
  function toPlacement(side, align) {
875
809
  return align === "center" ? side : `${side}-${align}`;
876
810
  }
@@ -969,12 +903,22 @@ function Content({
969
903
  },
970
904
  [setFloating]
971
905
  );
972
- useClickOutside(
973
- [contentRef, triggerRef, inputRef, input0Ref, input1Ref],
974
- () => dispatch({ type: "CLOSE" }),
975
- isOpen
976
- );
977
- useFocusTrap(contentRef, isOpen);
906
+ const close = useCallback(() => {
907
+ const source = state.openSource;
908
+ dispatch({ type: "CLOSE" });
909
+ restoreFocus({
910
+ openSource: source === "input" ? "input" : source === "trigger" ? "trigger" : "unknown",
911
+ trigger: document.getElementById(ids.trigger),
912
+ input: document.getElementById(ids.input) ?? document.getElementById(`${ids.input}-0`)
913
+ });
914
+ }, [dispatch, ids.input, ids.trigger, state.openSource]);
915
+ useClickOutside([contentRef, triggerRef, inputRef, input0Ref, input1Ref], close, isOpen);
916
+ useEscapeKey({
917
+ enabled: isOpen,
918
+ stopPropagation: true,
919
+ onEscape: close
920
+ });
921
+ useFocusTrap(contentRef, isOpen && config.modal);
978
922
  const [transitionsReady, setTransitionsReady] = useState(false);
979
923
  useEffect(() => {
980
924
  if (!isOpen || !isPositioned) {
@@ -1003,7 +947,7 @@ function Content({
1003
947
  ref: mergedRef,
1004
948
  id: ids.content,
1005
949
  role: "dialog",
1006
- "aria-modal": "true",
950
+ "aria-modal": config.modal ? "true" : void 0,
1007
951
  "aria-labelledby": ids.label,
1008
952
  "data-state": isOpen ? "open" : "closed",
1009
953
  style: {
@@ -1017,14 +961,7 @@ function Content({
1017
961
  ...isOpen && !transitionsReady ? { transition: "none" } : void 0,
1018
962
  ...style
1019
963
  },
1020
- onKeyDown: (e) => {
1021
- if (e.key === "Escape") {
1022
- e.preventDefault();
1023
- dispatch({ type: "CLOSE" });
1024
- document.getElementById(ids.trigger)?.focus();
1025
- }
1026
- onKeyDown?.(e);
1027
- },
964
+ onKeyDown,
1028
965
  ...props,
1029
966
  children: [
1030
967
  /* @__PURE__ */ jsx(