rn-grid-date-time-picker 1.2.2 → 1.3.1

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/lib/commonjs/components/DateTimePicker.js +109 -129
  3. package/lib/commonjs/components/DateTimePicker.js.map +1 -1
  4. package/lib/commonjs/components/calendar/DayCell.js +17 -6
  5. package/lib/commonjs/components/calendar/DayCell.js.map +1 -1
  6. package/lib/commonjs/components/calendar/calendarStyles.js +11 -0
  7. package/lib/commonjs/components/calendar/calendarStyles.js.map +1 -1
  8. package/lib/commonjs/components/container/PickerContainer.js +2 -1
  9. package/lib/commonjs/components/container/PickerContainer.js.map +1 -1
  10. package/lib/commonjs/components/container/PickerLayout.js +5 -1
  11. package/lib/commonjs/components/container/PickerLayout.js.map +1 -1
  12. package/lib/commonjs/components/field/ValueField.js +19 -2
  13. package/lib/commonjs/components/field/ValueField.js.map +1 -1
  14. package/lib/commonjs/components/field/fieldText.js +44 -5
  15. package/lib/commonjs/components/field/fieldText.js.map +1 -1
  16. package/lib/commonjs/components/hooks/useScrollToTime.js +85 -0
  17. package/lib/commonjs/components/hooks/useScrollToTime.js.map +1 -0
  18. package/lib/commonjs/components/theme/styleKeys.js +3 -1
  19. package/lib/commonjs/components/theme/styleKeys.js.map +1 -1
  20. package/lib/commonjs/components/time/TypedTime.js +20 -2
  21. package/lib/commonjs/components/time/TypedTime.js.map +1 -1
  22. package/lib/commonjs/components/types.js +4 -1
  23. package/lib/commonjs/components/types.js.map +1 -1
  24. package/lib/commonjs/core/draft.js +2 -1
  25. package/lib/commonjs/core/draft.js.map +1 -1
  26. package/lib/module/components/DateTimePicker.js +110 -130
  27. package/lib/module/components/DateTimePicker.js.map +1 -1
  28. package/lib/module/components/calendar/DayCell.js +18 -7
  29. package/lib/module/components/calendar/DayCell.js.map +1 -1
  30. package/lib/module/components/calendar/calendarStyles.js +11 -0
  31. package/lib/module/components/calendar/calendarStyles.js.map +1 -1
  32. package/lib/module/components/container/PickerContainer.js +2 -1
  33. package/lib/module/components/container/PickerContainer.js.map +1 -1
  34. package/lib/module/components/container/PickerLayout.js +5 -1
  35. package/lib/module/components/container/PickerLayout.js.map +1 -1
  36. package/lib/module/components/field/ValueField.js +20 -3
  37. package/lib/module/components/field/ValueField.js.map +1 -1
  38. package/lib/module/components/field/fieldText.js +40 -4
  39. package/lib/module/components/field/fieldText.js.map +1 -1
  40. package/lib/module/components/hooks/useScrollToTime.js +79 -0
  41. package/lib/module/components/hooks/useScrollToTime.js.map +1 -0
  42. package/lib/module/components/theme/styleKeys.js +3 -1
  43. package/lib/module/components/theme/styleKeys.js.map +1 -1
  44. package/lib/module/components/time/TypedTime.js +22 -4
  45. package/lib/module/components/time/TypedTime.js.map +1 -1
  46. package/lib/module/components/types.js +4 -1
  47. package/lib/module/components/types.js.map +1 -1
  48. package/lib/module/core/draft.js +2 -1
  49. package/lib/module/core/draft.js.map +1 -1
  50. package/lib/typescript/commonjs/src/components/DateTimePicker.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/src/components/calendar/DayCell.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/src/components/calendar/calendarStyles.d.ts +9 -0
  53. package/lib/typescript/commonjs/src/components/calendar/calendarStyles.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/src/components/container/PickerContainer.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/src/components/container/PickerLayout.d.ts +7 -2
  56. package/lib/typescript/commonjs/src/components/container/PickerLayout.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/src/components/field/ValueField.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/src/components/field/fieldText.d.ts +19 -0
  59. package/lib/typescript/commonjs/src/components/field/fieldText.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/src/components/hooks/useScrollToTime.d.ts +26 -0
  61. package/lib/typescript/commonjs/src/components/hooks/useScrollToTime.d.ts.map +1 -0
  62. package/lib/typescript/commonjs/src/components/nativeTypes.d.ts +2 -1
  63. package/lib/typescript/commonjs/src/components/nativeTypes.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/src/components/theme/styleKeys.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/src/components/time/TypedTime.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/src/components/types.d.ts +59 -8
  67. package/lib/typescript/commonjs/src/components/types.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/src/core/draft.d.ts +2 -1
  69. package/lib/typescript/commonjs/src/core/draft.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/src/index.d.ts +1 -1
  71. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  72. package/lib/typescript/module/src/components/DateTimePicker.d.ts.map +1 -1
  73. package/lib/typescript/module/src/components/calendar/DayCell.d.ts.map +1 -1
  74. package/lib/typescript/module/src/components/calendar/calendarStyles.d.ts +9 -0
  75. package/lib/typescript/module/src/components/calendar/calendarStyles.d.ts.map +1 -1
  76. package/lib/typescript/module/src/components/container/PickerContainer.d.ts.map +1 -1
  77. package/lib/typescript/module/src/components/container/PickerLayout.d.ts +7 -2
  78. package/lib/typescript/module/src/components/container/PickerLayout.d.ts.map +1 -1
  79. package/lib/typescript/module/src/components/field/ValueField.d.ts.map +1 -1
  80. package/lib/typescript/module/src/components/field/fieldText.d.ts +19 -0
  81. package/lib/typescript/module/src/components/field/fieldText.d.ts.map +1 -1
  82. package/lib/typescript/module/src/components/hooks/useScrollToTime.d.ts +26 -0
  83. package/lib/typescript/module/src/components/hooks/useScrollToTime.d.ts.map +1 -0
  84. package/lib/typescript/module/src/components/nativeTypes.d.ts +2 -1
  85. package/lib/typescript/module/src/components/nativeTypes.d.ts.map +1 -1
  86. package/lib/typescript/module/src/components/theme/styleKeys.d.ts.map +1 -1
  87. package/lib/typescript/module/src/components/time/TypedTime.d.ts.map +1 -1
  88. package/lib/typescript/module/src/components/types.d.ts +59 -8
  89. package/lib/typescript/module/src/components/types.d.ts.map +1 -1
  90. package/lib/typescript/module/src/core/draft.d.ts +2 -1
  91. package/lib/typescript/module/src/core/draft.d.ts.map +1 -1
  92. package/lib/typescript/module/src/index.d.ts +1 -1
  93. package/lib/typescript/module/src/index.d.ts.map +1 -1
  94. package/package.json +1 -1
  95. package/src/components/DateTimePicker.tsx +141 -135
  96. package/src/components/calendar/DayCell.tsx +25 -6
  97. package/src/components/calendar/calendarStyles.ts +11 -0
  98. package/src/components/container/PickerContainer.tsx +1 -0
  99. package/src/components/container/PickerLayout.tsx +10 -1
  100. package/src/components/field/ValueField.tsx +30 -11
  101. package/src/components/field/fieldText.ts +48 -5
  102. package/src/components/hooks/useScrollToTime.ts +99 -0
  103. package/src/components/nativeTypes.ts +2 -1
  104. package/src/components/theme/styleKeys.ts +3 -1
  105. package/src/components/time/TypedTime.tsx +25 -6
  106. package/src/components/types.ts +63 -8
  107. package/src/core/draft.ts +5 -2
  108. package/src/index.tsx +1 -0
@@ -6,7 +6,7 @@ import { View } from 'react-native';
6
6
  import { resolveAdapter } from "../adapters/index.js";
7
7
  import { compareCalendarDates, daysBetween, isSameDay } from "../core/calendarDate.js";
8
8
  import { createConstraints } from "../core/constraints.js";
9
- import { clearDraft as emptied, createDraft, DEFAULT_TIME, draftToWire, sameDayRangeOk, setTime as applyTime, validateDraft } from "../core/draft.js";
9
+ import { createDraft, DEFAULT_TIME, draftToWire, sameDayRangeOk, setTime as applyTime, validateDraft } from "../core/draft.js";
10
10
  import { hijriDateText, hijriPeriods, isHijriSupported, resolveCalendarSystem } from "../core/calendarSystem.js";
11
11
  import { defaultLocale, deriveDateFormat, deriveFirstDayOfWeek, deriveTimeFormat, normalizeDateFormat } from "../core/locale.js";
12
12
  import { arabicDigits, latinDigits, localeDigits } from "../core/numerals.js";
@@ -17,7 +17,6 @@ import CalendarSwitch from "./calendar/CalendarSwitch.js";
17
17
  import { localeNames } from "./calendar/localeNames.js";
18
18
  import { RtlContext } from "./theme/direction.js";
19
19
  import { NumeralsContext } from "./theme/numerals.js";
20
- import { firstSelectedDay } from "./calendar/selection.js";
21
20
  import PickerContainer from "./container/PickerContainer.js";
22
21
  import { formatDateText, formatMultipleText, formatRangeText, formatValueText } from "./field/fieldText.js";
23
22
  import FieldMessages from "./field/FieldMessages.js";
@@ -25,6 +24,7 @@ import RangeFields from "./field/RangeFields.js";
25
24
  import ValueField from "./field/ValueField.js";
26
25
  import { useKeyboardFold } from "./hooks/useKeyboardFold.js";
27
26
  import { useOpenPicker } from "./hooks/useOpenPicker.js";
27
+ import { useScrollToTime } from "./hooks/useScrollToTime.js";
28
28
  import { usePickerDraft } from "./hooks/usePickerDraft.js";
29
29
  import { useTodayTick } from "./hooks/useTodayTick.js";
30
30
  import TimeRows from "./time/TimeRows.js";
@@ -40,19 +40,6 @@ const RTL = {
40
40
  direction: 'rtl'
41
41
  };
42
42
 
43
- /** The same kind of selection with nothing in it. */
44
- const emptySelection = sel => sel.kind === 'single' ? {
45
- kind: 'single',
46
- day: null
47
- } : sel.kind === 'range' ? {
48
- kind: 'range',
49
- from: null,
50
- to: null
51
- } : {
52
- ...sel,
53
- days: []
54
- };
55
-
56
43
  /** Minute steps that divide the hour. */
57
44
  const MINUTE_STEPS = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60];
58
45
 
@@ -89,6 +76,7 @@ const DateTimePicker = props => {
89
76
  multiple,
90
77
  range,
91
78
  presentation = 'sheet',
79
+ scrollToTimeRef,
92
80
  visible,
93
81
  onOpen,
94
82
  onClose,
@@ -139,6 +127,7 @@ const DateTimePicker = props => {
139
127
  calendarSwitch,
140
128
  onCalendarSystemChange,
141
129
  dualDefaultTab = 'gregory',
130
+ fieldCalendar = 'picked',
142
131
  hijriLocale = 'ar-SA',
143
132
  renderCalendarSwitch,
144
133
  showToday = false,
@@ -329,12 +318,13 @@ const DateTimePicker = props => {
329
318
  const folded = keyboardUp && (!isInline || ownFocus);
330
319
  const rangeLayout = range?.layout ?? 'singleField';
331
320
  const multipleTimeMode = multiple?.timeMode ?? 'shared';
321
+ const startsEmpty = newDayTime === 'empty';
332
322
  const draftOptions = useMemo(() => {
333
323
  return {
334
324
  type,
335
325
  zone,
336
326
  adapter,
337
- defaultTime: DEFAULT_TIME,
327
+ defaultTime: startsEmpty ? null : DEFAULT_TIME,
338
328
  multiple: {
339
329
  timeMode: multiple?.timeMode,
340
330
  maxSelections: multiple?.maxSelections
@@ -346,7 +336,7 @@ const DateTimePicker = props => {
346
336
  maxDays: range?.maxDays
347
337
  }
348
338
  };
349
- }, [type, zone, adapter, multiple?.timeMode, multiple?.maxSelections, range?.allowSameDay, range?.reselect, range?.minDays, range?.maxDays]);
339
+ }, [type, zone, adapter, startsEmpty, multiple?.timeMode, multiple?.maxSelections, range?.allowSameDay, range?.reselect, range?.minDays, range?.maxDays]);
350
340
  const draft = usePickerDraft(mode, value, isOpen, draftOptions);
351
341
  const today = useTodayTick(zone, now);
352
342
 
@@ -521,6 +511,15 @@ const DateTimePicker = props => {
521
511
  const preview = useMemo(() => Object.values(rowPending).reduce((acc, p) => p ? applyTime(acc, p.time, p.target) : acc, draft.draft), [draft.draft, rowPending]);
522
512
  const previewIssue = validateDraft(preview, draftOptions);
523
513
  const d = draft.draft;
514
+ // Once a calendar tap picks the date (both ends of a range), the sheet (or
515
+ // the app's ScrollView around an inline picker) scrolls the time section
516
+ // into view.
517
+ const datePicked = d.mode === 'single' ? !!d.slot.value : d.mode === 'range' ? !!d.from.value && !!d.to.value : d.slots.length > 0;
518
+ const {
519
+ scrollRef,
520
+ timeRef,
521
+ request: revealTime
522
+ } = useScrollToTime(type !== 'datetime' ? false : isInline ? !!scrollToTimeRef : sheetOptions?.scrollToTime ?? true, datePicked, isInline ? scrollToTimeRef : undefined);
524
523
  /** The values a draft would save, with a shared time applied. */
525
524
  const valuesOf = dr => {
526
525
  if (dr.mode === 'single') return [dr.slot.value];
@@ -557,10 +556,10 @@ const DateTimePicker = props => {
557
556
  };
558
557
  const valueKey = JSON.stringify(value ?? '');
559
558
 
560
- // A selection belongs to the calendar it was picked in (Gregorian or Hijri):
561
- // it shows only there, and the field writes it in that calendar. A saved
562
- // value remembers its calendar; one from outside counts as picked in the
563
- // calendar the picker opens on.
559
+ // A selection shows in both tabs (Gregorian and Hijri). It is saved with the
560
+ // calendar on screen, and the on-screen field writes it in that calendar. A
561
+ // saved value remembers its calendar; one from outside counts as picked in
562
+ // the calendar the picker opens on.
564
563
  // The saved value as a draft, for the on-screen field and for comparing
565
564
  // values by what they hold.
566
565
  const saved = useMemo(() => createDraft(mode, value, draftOptions),
@@ -571,32 +570,12 @@ const DateTimePicker = props => {
571
570
  const savedContent = contentOf(saved);
572
571
  const savedSystemRef = useRef(null);
573
572
  const savedSystem = savedSystemRef.current?.content === savedContent ? savedSystemRef.current.system : outsideSystem;
574
- const [draftSystem, setDraftSystem] = useState(savedSystem);
575
- // The draft is rebuilt on open and on a new value; so is its calendar. A new
576
- // calendarSystem prop while the user is picking starts the selection over,
577
- // since a selection is one calendar's.
578
- const systemSeen = useRef({
579
- isOpen,
580
- valueKey,
581
- savedSystem
582
- });
583
- useEffect(() => {
584
- const seen = systemSeen.current;
585
- systemSeen.current = {
586
- isOpen,
587
- valueKey,
588
- savedSystem
589
- };
590
- setDraftSystem(savedSystem);
591
- const systemOnly = seen.isOpen === isOpen && seen.valueKey === valueKey && seen.savedSystem !== savedSystem;
592
- if (systemOnly && isOpen && edited.current) {
593
- draft.clear();
594
- resetSelectionUi();
595
- }
596
- // eslint-disable-next-line react-hooks/exhaustive-deps -- the draft's actions are stable
597
- }, [isOpen, valueKey, savedSystem]);
598
- useEffect(() => setDualTab(dualDefaultTab), [isOpen, dualDefaultTab]);
599
- const selectionShown = draftSystem === shownSystem;
573
+ const hasSaved = valuesOf(saved).some(v => v !== null);
574
+ // Each open starts on the tab the saved value was saved with; with no value,
575
+ // on dualDefaultTab.
576
+ useEffect(() => setDualTab(hasSaved ? savedSystem : dualDefaultTab),
577
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- on open and on a new dualDefaultTab, not on every save
578
+ [isOpen, dualDefaultTab]);
600
579
  /** Saves a value: with the calendar it was picked in, when one can be Hijri. */
601
580
  const emitChange = useCallback((wire, picked) => {
602
581
  savedSystemRef.current = {
@@ -620,11 +599,12 @@ const DateTimePicker = props => {
620
599
  bounds: constraints.timeBounds(v.date)
621
600
  }) === null)) && !typingBlocked && !wheelSpinning && !confirmDisabled;
622
601
 
623
- // Every save: Confirm, an 'instant' edit (onConfirm is added by `confirm`).
602
+ // Every save: Confirm, an 'instant' edit (onConfirm is added by `confirm`),
603
+ // with the calendar on screen.
624
604
  const commit = useCallback(wire => {
625
- emitChange(wire, draftSystem);
605
+ emitChange(wire, shownSystem);
626
606
  onBlur?.();
627
- }, [emitChange, draftSystem, onBlur]);
607
+ }, [emitChange, shownSystem, onBlur]);
628
608
  const confirm = useCallback(() => {
629
609
  if (!canConfirm) return;
630
610
  const {
@@ -656,8 +636,8 @@ const DateTimePicker = props => {
656
636
  draft.clear();
657
637
  resetSelectionUi();
658
638
  edited.current = false;
659
- emitChange('', draftSystem);
660
- }, [draft, emitChange, draftSystem, resetSelectionUi]);
639
+ emitChange('', shownSystem);
640
+ }, [draft, emitChange, shownSystem, resetSelectionUi]);
661
641
  const clearDraft = useCallback(() => {
662
642
  edited.current = true;
663
643
  draft.clear();
@@ -666,7 +646,6 @@ const DateTimePicker = props => {
666
646
 
667
647
  // × in the field above the calendar. 'instant' saves every edit, so it
668
648
  // clears the saved value too (once; an empty value isn't saved again).
669
- const hasSaved = valuesOf(saved).some(v => v !== null);
670
649
  const clearInSheet = useCallback(() => {
671
650
  if (commitMode === 'instant' && hasSaved) clearValue();else clearDraft();
672
651
  }, [commitMode, hasSaved, clearValue, clearDraft]);
@@ -678,7 +657,8 @@ const DateTimePicker = props => {
678
657
  // Confirm press, but only while open (closing discards, as with Confirm),
679
658
  // and only what the draft holds: a time still being typed isn't saved yet.
680
659
  // An edit equal to the saved value isn't saved again; one `validate`
681
- // rejected waits until the selection or the saved value changes.
660
+ // rejected waits until the selection or the saved value changes. A tab
661
+ // change the user makes saves the selection again with the new calendar.
682
662
  const lastRejected = useRef(null);
683
663
  // The last value saved: not saved again while the parent's `value` catches
684
664
  // up (a slow store, a server round trip) or when it never does.
@@ -696,11 +676,12 @@ const DateTimePicker = props => {
696
676
  if (!typingDone || timeIssue || !canSave(draft.draft)) return;
697
677
  const wire = draft.toWire();
698
678
  const key = JSON.stringify(wire);
699
- if (key === lastRejected.current || key === lastEmitted.current) return;
700
- // The same dates picked in the same calendar aren't saved again; the same
701
- // dates picked in the other calendar are.
679
+ const sent = `${shownSystem} ${key}`;
680
+ if (key === lastRejected.current || sent === lastEmitted.current) return;
681
+ // The same dates in the same calendar aren't saved again; the same dates
682
+ // with the other tab on screen are.
702
683
  const unchanged = key === valueKey || contentOf(draft.draft) === savedContent;
703
- if (unchanged && draftSystem === savedSystem) return;
684
+ if (unchanged && shownSystem === savedSystem) return;
704
685
  const message = latest.current.validate?.(wire);
705
686
  if (message) {
706
687
  lastRejected.current = key;
@@ -711,14 +692,14 @@ const DateTimePicker = props => {
711
692
  latest.current.onError?.('VALIDATION', wire);
712
693
  return;
713
694
  }
714
- lastEmitted.current = key;
695
+ lastEmitted.current = sent;
715
696
  commit(wire);
716
697
  };
717
698
  // Only typing waits for the field to be left; a tap is a finished edit.
718
699
  const typedLast = useRef(false);
719
700
  useEffect(() => saveInstant(!sheetTyping || !typedLast.current),
720
701
  // eslint-disable-next-line react-hooks/exhaustive-deps -- when the draft or what blocks it changes, not when callbacks are re-created
721
- [commitMode, isOpen, sheetTyping, draft.draft, draftSystem, timeIssue, typingBlocked, wheelSpinning, confirmDisabled, constraints]);
702
+ [commitMode, isOpen, sheetTyping, draft.draft, shownSystem, timeIssue, typingBlocked, wheelSpinning, confirmDisabled, constraints]);
722
703
  // Closing while the sheet's field still has focus ends the typing: an
723
704
  // instant picker saves what was typed before it closes.
724
705
  const saveInstantRef = useRef(saveInstant);
@@ -729,21 +710,14 @@ const DateTimePicker = props => {
729
710
  }, [closePicker]);
730
711
 
731
712
  // A typed date with no time yet gets one the way a tapped day does: the fixed
732
- // newDayTime, else a time already typed in the time row, else now.
713
+ // newDayTime, else a time already typed in the time row, else now (or none).
733
714
  const withFilledTime = (parsed, key) => parsed.value ?? (parsed.dateOnly ? {
734
715
  date: parsed.dateOnly,
735
- time: fixedTime ?? pending[key] ?? nowTime()
716
+ time: fixedTime ?? pending[key] ?? newTime()
736
717
  } : null);
737
-
738
- // Typed segments are Gregorian, so typing keeps the calendar a selection was
739
- // picked in; into an empty draft, it takes the calendar on screen.
740
- const adoptShownIfEmpty = () => {
741
- if (valuesOf(d).every(v => v === null)) setDraftSystem(shownSystem);
742
- };
743
718
  const onSheetTyped = parsed => {
744
719
  edited.current = true;
745
720
  typedLast.current = true;
746
- adoptShownIfEmpty();
747
721
  const typed = withFilledTime(parsed, 'single');
748
722
  setTypingBlock('single', !parsed.isEmpty && !typed);
749
723
  if (typed) draft.setValue(typed, 'single');else if (parsed.isEmpty) draft.clear();
@@ -751,14 +725,13 @@ const DateTimePicker = props => {
751
725
  const onRangeTyped = (end, parsed) => {
752
726
  edited.current = true;
753
727
  typedLast.current = true;
754
- adoptShownIfEmpty();
755
728
  const typed = withFilledTime(parsed, end);
756
729
  setTypingBlock(end, !parsed.isEmpty && !typed);
757
730
  if (typed || parsed.isEmpty) draft.setValue(typed, end);
758
731
  };
759
732
 
760
733
  // A fixed time for every newly picked day (wins over a typed time and over "now").
761
- const fixedTime = newDayTime ?? null;
734
+ const fixedTime = typeof newDayTime === 'object' ? newDayTime : null;
762
735
 
763
736
  // A day picked on an empty date-time starts at the current time, moved up to
764
737
  // the next allowed minute step. An existing time is kept.
@@ -772,6 +745,8 @@ const DateTimePicker = props => {
772
745
  minute: capped % 60
773
746
  };
774
747
  }, [now, zone, minuteStep]);
748
+ /** A new day's time when nothing fixed or typed: now, or none with newDayTime 'empty'. */
749
+ const newTime = useCallback(() => startsEmpty ? null : nowTime(), [startsEmpty, nowTime]);
775
750
 
776
751
  // A range end picked on the start's day starts an hour after the start (at
777
752
  // most the day's latest allowed time, or its last step), so the range isn't
@@ -779,8 +754,8 @@ const DateTimePicker = props => {
779
754
  // A time typed for the end before its day is kept as typed.
780
755
  const endTimeFor = (from, day, typed) => {
781
756
  if (!fixedTime && typed) return typed;
782
- const time = fixedTime ?? nowTime();
783
- if (!from?.time || !isSameDay(from.date, day)) return time;
757
+ const time = fixedTime ?? newTime();
758
+ if (!time || !from?.time || !isSameDay(from.date, day)) return time;
784
759
  const start = from.time.hour * 60 + from.time.minute;
785
760
  if (time.hour * 60 + time.minute > start) return time;
786
761
  // Within the day's allowed hours when it has them.
@@ -795,66 +770,57 @@ const DateTimePicker = props => {
795
770
  const onSelectDay = useCallback(day => {
796
771
  edited.current = true;
797
772
  typedLast.current = false;
798
- // A pick in the other calendar starts over: the selection is one calendar's.
799
- const fresh = draftSystem !== shownSystem && valuesOf(d).some(v => v !== null);
800
- setDraftSystem(shownSystem);
801
- const cur = fresh ? emptied(d) : d;
802
- const end0 = fresh ? 'from' : activeEnd;
803
- const dayInEdit = fresh ? null : activeDay;
804
- const times = fresh ? {} : pending;
805
- if (fresh) {
806
- draft.clear();
807
- resetSelectionUi();
808
- }
809
- if (cur.mode === 'range') {
773
+ // A multiple picker scrolls for its first date only, so more can be picked.
774
+ if (d.mode !== 'multiple' || d.slots.length === 0) revealTime();
775
+ if (d.mode === 'range') {
810
776
  if (rangeLayout === 'fromTo') {
811
777
  setEndPicked(false);
812
778
  // A full range: the tap starts a new one (or moves the nearest end).
813
- if (!endPicked && cur.from.value && cur.to.value) {
779
+ if (!endPicked && d.from.value && d.to.value) {
814
780
  const moveNearest = range?.reselect === 'moveNearestEnd';
815
- const nearest = Math.abs(daysBetween(cur.from.value.date, day)) < Math.abs(daysBetween(cur.to.value.date, day)) ? 'from' : 'to';
781
+ const nearest = Math.abs(daysBetween(d.from.value.date, day)) < Math.abs(daysBetween(d.to.value.date, day)) ? 'from' : 'to';
816
782
  const end = moveNearest ? nearest : 'from';
817
783
  setTypingBlock('from', false);
818
784
  setTypingBlock('to', false);
819
- draft.selectDay(day, fixedTime ?? times[end] ?? nowTime());
785
+ draft.selectDay(day, fixedTime ?? pending[end] ?? newTime());
820
786
  setActiveEnd(moveNearest ? nearest : 'to');
821
787
  return;
822
788
  }
823
789
  // Choosing the end after a start tap: a day before the start moves the start.
824
- const end1 = end0 === 'to' && !endPicked && cur.from.value && compareCalendarDates(day, cur.from.value.date) < 0 ? 'from' : end0;
790
+ const end1 = activeEnd === 'to' && !endPicked && d.from.value && compareCalendarDates(day, d.from.value.date) < 0 ? 'from' : activeEnd;
825
791
  // The tapped day replaces whatever was typed in this end only.
826
792
  setTypingBlock(end1, false);
827
- draft.setRangeEnd(day, end1, end1 === 'to' ? endTimeFor(cur.from.value, day, times.to) : fixedTime ?? times.from ?? nowTime());
793
+ draft.setRangeEnd(day, end1, end1 === 'to' ? endTimeFor(d.from.value, day, pending.to) : fixedTime ?? pending.from ?? newTime());
828
794
  // The start tapped again where a range can't be one day deselects
829
795
  // it, so the next tap picks the start again.
830
- const deselects = end1 === 'to' && !sameDayRangeOk(draftOptions) && isSameDay(cur.from.value?.date, day);
831
- if (end0 === 'from') setActiveEnd('to');else if (deselects) setActiveEnd('from');
796
+ const deselects = end1 === 'to' && !sameDayRangeOk(draftOptions) && isSameDay(d.from.value?.date, day);
797
+ if (activeEnd === 'from') setActiveEnd('to');else if (deselects) setActiveEnd('from');
832
798
  return;
833
799
  }
834
800
  // The single field: the first tap is the start, the next one the end.
835
- const startsOver = !cur.from.value || !!cur.to.value && range?.reselect !== 'moveNearestEnd';
801
+ const startsOver = !d.from.value || !!d.to.value && range?.reselect !== 'moveNearestEnd';
836
802
  const end = startsOver ? 'from' : 'to';
837
803
  setTypingBlock('single', false);
838
- draft.selectDay(day, end === 'to' && !cur.to.value ? endTimeFor(cur.from.value, day, times.to) : fixedTime ?? times[end] ?? nowTime());
804
+ draft.selectDay(day, end === 'to' && !d.to.value ? endTimeFor(d.from.value, day, pending.to) : fixedTime ?? pending[end] ?? newTime());
839
805
  return;
840
806
  }
841
- if (cur.mode === 'multiple') {
807
+ if (d.mode === 'multiple') {
842
808
  const perDate = type === 'datetime' && multipleTimeMode === 'perDate';
843
- const picked = cur.slots.some(slot => isSameDay(slot.value?.date, day));
809
+ const picked = d.slots.some(slot => isSameDay(slot.value?.date, day));
844
810
  // perDate: a tap on a picked day first makes it the one the time row edits.
845
- if (perDate && picked && !isSameDay(dayInEdit, day)) {
811
+ if (perDate && picked && !isSameDay(activeDay, day)) {
846
812
  setActiveDay(day);
847
813
  return;
848
814
  }
849
- draft.selectDay(day, fixedTime ?? nowTime());
815
+ draft.selectDay(day, fixedTime ?? newTime());
850
816
  if (perDate) setActiveDay(picked ? null : day);
851
817
  return;
852
818
  }
853
819
  setTypingBlock('single', false);
854
- draft.selectDay(day, fixedTime ?? times.single ?? nowTime());
820
+ draft.selectDay(day, fixedTime ?? pending.single ?? newTime());
855
821
  },
856
822
  // eslint-disable-next-line react-hooks/exhaustive-deps -- valuesOf reads only `d` and props listed here
857
- [d, draft, nowTime, rangeLayout, activeEnd, activeDay, fixedTime, pending, range?.reselect, type, multipleTimeMode, setTypingBlock, draftSystem, shownSystem, resetSelectionUi, endPicked, draftOptions, minuteStep, constraints]);
823
+ [d, draft, newTime, rangeLayout, activeEnd, activeDay, fixedTime, pending, range?.reselect, type, multipleTimeMode, setTypingBlock, endPicked, draftOptions, minuteStep, constraints, revealTime]);
858
824
 
859
825
  // The calendar gets a tap handler that never changes, so its memoized day
860
826
  // cells don't all redraw on every tap; it calls the current one.
@@ -974,12 +940,13 @@ const DateTimePicker = props => {
974
940
 
975
941
  /** A value's date in the calendar it was picked in: Hijri text, or none for the dateFormat. */
976
942
  const dateTextFor = picked => picked === 'hijri' ? v => hijriDateText(v.date, hijriLocale) ?? formatDateText(v, dateFormat) : undefined;
977
- const savedDateText = dateTextFor(savedSystem);
978
- const draftDateText = dateTextFor(draftSystem);
979
- const savedStrings = stringsFor(savedSystem);
980
- const draftStrings = stringsFor(draftSystem);
981
- // Error messages inside the picker follow the tab on screen; the on-screen
982
- // field's keep `strings`.
943
+ // The calendar the on-screen field writes the saved value in.
944
+ const fieldSystem = fieldCalendar === 'gregory' ? 'gregory' : savedSystem;
945
+ const savedDateText = dateTextFor(fieldSystem);
946
+ const draftDateText = dateTextFor(shownSystem);
947
+ const savedStrings = stringsFor(fieldSystem);
948
+ // The texts inside the picker, its error messages too, follow the tab on
949
+ // screen; the on-screen field's keep `strings`.
983
950
  const shownStrings = stringsFor(shownSystem);
984
951
 
985
952
  // What the on-screen field shows: the saved value, never the draft.
@@ -1010,7 +977,7 @@ const DateTimePicker = props => {
1010
977
  type,
1011
978
  mode,
1012
979
  value,
1013
- displayText: digitsFor(savedSystem)(savedText ?? formatValueText(savedSingle, type, dateFormat, timeFormat, savedDateText, savedStrings)),
980
+ displayText: digitsFor(fieldSystem)(savedText ?? formatValueText(savedSingle, type, dateFormat, timeFormat, savedDateText, savedStrings)),
1014
981
  disabled,
1015
982
  readOnly,
1016
983
  isOpen,
@@ -1031,11 +998,12 @@ const DateTimePicker = props => {
1031
998
  };
1032
999
  const sheetValue = draft.draft.mode === 'single' ? draft.draft.slot.value : null;
1033
1000
  // A time outside the limits is explained under the time row, not here.
1034
- const sheetValueError = sheetValue && dayOff(sheetValue.date) ? shownStrings.errorNotAllowed : '';
1001
+ const notAllowedError = sheetOptions?.showNotAllowedError === false ? '' : shownStrings.errorNotAllowed;
1002
+ const sheetValueError = sheetValue && dayOff(sheetValue.date) ? notAllowedError : '';
1035
1003
  let rangeError = '';
1036
1004
  if (d.mode === 'range' && d.from.value && d.to.value) {
1037
1005
  const length = daysBetween(d.from.value.date, d.to.value.date) + 1;
1038
- if (constraints.dayStatus(d.from.value.date).disabled || constraints.dayStatus(d.to.value.date).disabled) rangeError = shownStrings.errorNotAllowed;else if (previewIssue === 'RANGE_ORDER') rangeError = shownStrings.errorRangeOrder;else if (previewIssue === 'RANGE_LENGTH') rangeError = range?.minDays !== undefined && length < range.minDays ? shownStrings.errorRangeTooShort(range.minDays) : shownStrings.errorRangeTooLong(range?.maxDays ?? length);
1006
+ if (constraints.dayStatus(d.from.value.date).disabled || constraints.dayStatus(d.to.value.date).disabled) rangeError = notAllowedError;else if (previewIssue === 'RANGE_ORDER') rangeError = shownStrings.errorRangeOrder;else if (previewIssue === 'RANGE_LENGTH') rangeError = range?.minDays !== undefined && length < range.minDays ? shownStrings.errorRangeTooShort(range.minDays) : shownStrings.errorRangeTooLong(range?.maxDays ?? length);
1039
1007
  }
1040
1008
  const fieldCommon = {
1041
1009
  type,
@@ -1085,7 +1053,7 @@ const DateTimePicker = props => {
1085
1053
  testID: testID ? `${testID}-value` : undefined
1086
1054
  }) : mode === 'single' ? /*#__PURE__*/_jsx(ValueField, {
1087
1055
  ...fieldCommon,
1088
- strings: draftStrings,
1056
+ strings: shownStrings,
1089
1057
  errorStrings: shownStrings,
1090
1058
  value: sheetValue,
1091
1059
  displayText: precision !== 'day' ? periodText(sheetValue) : undefined,
@@ -1102,7 +1070,7 @@ const DateTimePicker = props => {
1102
1070
  testID: testID ? `${testID}-value` : undefined
1103
1071
  }) : d.mode === 'range' ? /*#__PURE__*/_jsx(RangeFields, {
1104
1072
  ...fieldCommon,
1105
- strings: draftStrings,
1073
+ strings: shownStrings,
1106
1074
  errorStrings: shownStrings,
1107
1075
  layout: rangeLayout,
1108
1076
  dateText: draftDateText,
@@ -1122,9 +1090,9 @@ const DateTimePicker = props => {
1122
1090
  testID: testID
1123
1091
  }) : d.mode === 'multiple' ? /*#__PURE__*/_jsx(ValueField, {
1124
1092
  ...fieldCommon,
1125
- strings: draftStrings,
1093
+ strings: shownStrings,
1126
1094
  value: null,
1127
- displayText: formatMultipleText(draftValues, type, dateFormat, timeFormat, draftDateText, draftStrings),
1095
+ displayText: formatMultipleText(draftValues, type, dateFormat, timeFormat, draftDateText, shownStrings),
1128
1096
  placeholder: sheetOptions?.fieldPlaceholder,
1129
1097
  allowClear: allowClear,
1130
1098
  onClear: clearInSheet,
@@ -1139,13 +1107,20 @@ const DateTimePicker = props => {
1139
1107
  dual: strings.calendarDual ?? DEFAULT_STRINGS.calendarDual
1140
1108
  };
1141
1109
  const systemA11yLabel = strings.a11yCalendarSystem ?? DEFAULT_STRINGS.a11yCalendarSystem;
1110
+ // A calendar or tab the user picks is an edit: 'instant' saves the selection
1111
+ // again with that calendar.
1142
1112
  const chooseSystem = next => {
1143
1113
  if (next === system) return;
1114
+ edited.current = true;
1144
1115
  setChosenSystem(next);
1145
1116
  if (next === 'dual') setDualTab(dualDefaultTab);
1146
1117
  echo.current = next;
1147
1118
  onCalendarSystemChange?.(next);
1148
1119
  };
1120
+ const chooseTab = next => {
1121
+ edited.current = true;
1122
+ setDualTab(next);
1123
+ };
1149
1124
  const calendarSwitchNode = !switchSystems ? null : renderCalendarSwitch ? renderCalendarSwitch({
1150
1125
  systems: switchSystems,
1151
1126
  selected: system,
@@ -1177,7 +1152,7 @@ const DateTimePicker = props => {
1177
1152
  label: systemLabels.hijri
1178
1153
  }],
1179
1154
  selected: dualTab,
1180
- onSelect: setDualTab,
1155
+ onSelect: chooseTab,
1181
1156
  accessibilityLabel: systemA11yLabel,
1182
1157
  tokens: tokens,
1183
1158
  styles: styles,
@@ -1193,16 +1168,16 @@ const DateTimePicker = props => {
1193
1168
  children: node
1194
1169
  })
1195
1170
  });
1196
- // Another calendar opens on the days the last one showed, else on the
1197
- // selection (shown or not), else where the picker would open.
1198
- const calendarAnchor = shownAnchor.current ?? (selectionShown ? null : firstSelectedDay(selection)) ?? initialVisibleDate;
1171
+ // Another calendar opens on the days the last one showed, else where the
1172
+ // picker would open.
1173
+ const calendarAnchor = shownAnchor.current ?? initialVisibleDate;
1199
1174
  const calendar = /*#__PURE__*/_jsxs(View, {
1200
1175
  style: folded ? FOLDED : undefined,
1201
1176
  children: [calendarSwitchNode, dualTabs, mirrored(/*#__PURE__*/_jsx(Calendar, {
1202
- constraints: selectionShown ? calendarConstraints : constraints,
1177
+ constraints: calendarConstraints,
1203
1178
  today: today,
1204
1179
  initialVisibleDate: calendarAnchor,
1205
- selection: selectionShown ? selection : emptySelection(selection),
1180
+ selection: selection,
1206
1181
  onSelectDay: onSelectDayStable,
1207
1182
  firstDayOfWeek: firstDayOfWeek,
1208
1183
  showOutsideDays: showOutsideDays,
@@ -1265,18 +1240,22 @@ const DateTimePicker = props => {
1265
1240
  // Shown while the selection is still the one it was given for.
1266
1241
  const validationMessage = validation && (validation.key === JSON.stringify(draft.toWire()) || validation.key === JSON.stringify(draftToWire(preview, draftOptions))) ? validation.message : null;
1267
1242
 
1268
- // The field writes the date in the digits of the calendar it was picked in;
1269
- // the calendar and the time rows follow the tab that shows.
1243
+ // The field, the calendar and the time rows follow the tab that shows.
1270
1244
  const sheetContent = /*#__PURE__*/_jsxs(_Fragment, {
1271
- children: [/*#__PURE__*/_jsx(NumeralsContext.Provider, {
1272
- value: digitsFor(draftSystem),
1273
- children: sheetField
1274
- }), /*#__PURE__*/_jsx(NumeralsContext.Provider, {
1275
- value: digitsFor(shownSystem),
1276
- children: calendar
1277
- }), /*#__PURE__*/_jsx(NumeralsContext.Provider, {
1245
+ children: [/*#__PURE__*/_jsxs(NumeralsContext.Provider, {
1278
1246
  value: digitsFor(shownSystem),
1279
- children: mirrored(timeRow)
1247
+ children: [sheetField, calendar]
1248
+ }), timeRow == null ? null :
1249
+ /*#__PURE__*/
1250
+ // Measured for the scroll to the time (sheet.scrollToTime).
1251
+ _jsx(View, {
1252
+ ref: timeRef,
1253
+ collapsable: false,
1254
+ testID: testID ? `${testID}-time-section` : undefined,
1255
+ children: /*#__PURE__*/_jsx(NumeralsContext.Provider, {
1256
+ value: digitsFor(shownSystem),
1257
+ children: mirrored(timeRow)
1258
+ })
1280
1259
  }), validationMessage ? /*#__PURE__*/_jsx(FieldMessages, {
1281
1260
  error: validationMessage,
1282
1261
  tokens: tokens,
@@ -1288,13 +1267,14 @@ const DateTimePicker = props => {
1288
1267
  }) : null]
1289
1268
  });
1290
1269
  return /*#__PURE__*/_jsxs(NumeralsContext.Provider, {
1291
- value: digitsFor(savedSystem),
1270
+ value: digitsFor(fieldSystem),
1292
1271
  children: [trigger, /*#__PURE__*/_jsx(PickerContainer, {
1293
1272
  presentation: presentation,
1294
1273
  isOpen: isOpen,
1295
1274
  onRequestClose: close,
1296
1275
  renderSheet: renderSheet,
1297
1276
  sheet: sheetOptions,
1277
+ scrollRef: scrollRef,
1298
1278
  state: state,
1299
1279
  header: header,
1300
1280
  title: title ?? (isInline ? undefined : strings.titleDate),