@kenos-ui/react-datepicker 0.3.1 → 0.3.3

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,26 @@ function Content({
969
903
  },
970
904
  [setFloating]
971
905
  );
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]);
972
915
  useClickOutside(
973
916
  [contentRef, triggerRef, inputRef, input0Ref, input1Ref],
974
- () => dispatch({ type: "CLOSE" }),
917
+ close,
975
918
  isOpen
976
919
  );
977
- useFocusTrap(contentRef, isOpen);
920
+ useEscapeKey({
921
+ enabled: isOpen,
922
+ stopPropagation: true,
923
+ onEscape: close
924
+ });
925
+ useFocusTrap(contentRef, isOpen && config.modal);
978
926
  const [transitionsReady, setTransitionsReady] = useState(false);
979
927
  useEffect(() => {
980
928
  if (!isOpen || !isPositioned) {
@@ -1003,7 +951,7 @@ function Content({
1003
951
  ref: mergedRef,
1004
952
  id: ids.content,
1005
953
  role: "dialog",
1006
- "aria-modal": "true",
954
+ "aria-modal": config.modal ? "true" : void 0,
1007
955
  "aria-labelledby": ids.label,
1008
956
  "data-state": isOpen ? "open" : "closed",
1009
957
  style: {
@@ -1017,14 +965,7 @@ function Content({
1017
965
  ...isOpen && !transitionsReady ? { transition: "none" } : void 0,
1018
966
  ...style
1019
967
  },
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
- },
968
+ onKeyDown,
1028
969
  ...props,
1029
970
  children: [
1030
971
  /* @__PURE__ */ jsx(