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
@@ -15,7 +15,6 @@ import {
15
15
  } from '../core/calendarDate';
16
16
  import { createConstraints } from '../core/constraints';
17
17
  import {
18
- clearDraft as emptied,
19
18
  createDraft,
20
19
  DEFAULT_TIME,
21
20
  draftToWire,
@@ -56,7 +55,7 @@ import CalendarSwitch from './calendar/CalendarSwitch';
56
55
  import { localeNames } from './calendar/localeNames';
57
56
  import { RtlContext } from './theme/direction';
58
57
  import { NumeralsContext } from './theme/numerals';
59
- import { firstSelectedDay, type CalendarSelection } from './calendar/selection';
58
+ import type { CalendarSelection } from './calendar/selection';
60
59
  import PickerContainer from './container/PickerContainer';
61
60
  import {
62
61
  formatDateText,
@@ -70,6 +69,7 @@ import RangeFields, { type RangeEnd } from './field/RangeFields';
70
69
  import ValueField from './field/ValueField';
71
70
  import { useKeyboardFold } from './hooks/useKeyboardFold';
72
71
  import { useOpenPicker } from './hooks/useOpenPicker';
72
+ import { useScrollToTime } from './hooks/useScrollToTime';
73
73
  import { usePickerDraft } from './hooks/usePickerDraft';
74
74
  import { useTodayTick } from './hooks/useTodayTick';
75
75
  import TimeRows, { type PendingTimes } from './time/TimeRows';
@@ -94,14 +94,6 @@ const sameValue = (a: DateTimeValue, b: DateTimeValue): boolean =>
94
94
  const ALL_SYSTEMS: CalendarSystem[] = ['gregory', 'hijri', 'dual'];
95
95
  const RTL = { direction: 'rtl' } as const;
96
96
 
97
- /** The same kind of selection with nothing in it. */
98
- const emptySelection = (sel: CalendarSelection): CalendarSelection =>
99
- sel.kind === 'single'
100
- ? { kind: 'single', day: null }
101
- : sel.kind === 'range'
102
- ? { kind: 'range', from: null, to: null }
103
- : { ...sel, days: [] };
104
-
105
97
  /** Minute steps that divide the hour. */
106
98
  const MINUTE_STEPS = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60];
107
99
 
@@ -138,6 +130,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
138
130
  multiple,
139
131
  range,
140
132
  presentation = 'sheet',
133
+ scrollToTimeRef,
141
134
  visible,
142
135
  onOpen,
143
136
  onClose,
@@ -188,6 +181,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
188
181
  calendarSwitch,
189
182
  onCalendarSystemChange,
190
183
  dualDefaultTab = 'gregory',
184
+ fieldCalendar = 'picked',
191
185
  hijriLocale = 'ar-SA',
192
186
  renderCalendarSwitch,
193
187
  showToday = false,
@@ -432,12 +426,13 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
432
426
 
433
427
  const rangeLayout = range?.layout ?? 'singleField';
434
428
  const multipleTimeMode = multiple?.timeMode ?? 'shared';
429
+ const startsEmpty = newDayTime === 'empty';
435
430
  const draftOptions = useMemo((): DraftOptions => {
436
431
  return {
437
432
  type,
438
433
  zone,
439
434
  adapter,
440
- defaultTime: DEFAULT_TIME,
435
+ defaultTime: startsEmpty ? null : DEFAULT_TIME,
441
436
  multiple: {
442
437
  timeMode: multiple?.timeMode,
443
438
  maxSelections: multiple?.maxSelections,
@@ -453,6 +448,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
453
448
  type,
454
449
  zone,
455
450
  adapter,
451
+ startsEmpty,
456
452
  multiple?.timeMode,
457
453
  multiple?.maxSelections,
458
454
  range?.allowSameDay,
@@ -672,6 +668,28 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
672
668
  const previewIssue = validateDraft(preview, draftOptions);
673
669
 
674
670
  const d = draft.draft;
671
+ // Once a calendar tap picks the date (both ends of a range), the sheet (or
672
+ // the app's ScrollView around an inline picker) scrolls the time section
673
+ // into view.
674
+ const datePicked =
675
+ d.mode === 'single'
676
+ ? !!d.slot.value
677
+ : d.mode === 'range'
678
+ ? !!d.from.value && !!d.to.value
679
+ : d.slots.length > 0;
680
+ const {
681
+ scrollRef,
682
+ timeRef,
683
+ request: revealTime,
684
+ } = useScrollToTime(
685
+ type !== 'datetime'
686
+ ? false
687
+ : isInline
688
+ ? !!scrollToTimeRef
689
+ : (sheetOptions?.scrollToTime ?? true),
690
+ datePicked,
691
+ isInline ? scrollToTimeRef : undefined
692
+ );
675
693
  /** The values a draft would save, with a shared time applied. */
676
694
  const valuesOf = (dr: Draft): (DateTimeValue | null)[] => {
677
695
  if (dr.mode === 'single') return [dr.slot.value];
@@ -714,10 +732,10 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
714
732
  latest.current = { validate, onError, value };
715
733
  const valueKey = JSON.stringify(value ?? '');
716
734
 
717
- // A selection belongs to the calendar it was picked in (Gregorian or Hijri):
718
- // it shows only there, and the field writes it in that calendar. A saved
719
- // value remembers its calendar; one from outside counts as picked in the
720
- // calendar the picker opens on.
735
+ // A selection shows in both tabs (Gregorian and Hijri). It is saved with the
736
+ // calendar on screen, and the on-screen field writes it in that calendar. A
737
+ // saved value remembers its calendar; one from outside counts as picked in
738
+ // the calendar the picker opens on.
721
739
  // The saved value as a draft, for the on-screen field and for comparing
722
740
  // values by what they hold.
723
741
  const saved = useMemo(
@@ -736,27 +754,14 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
736
754
  savedSystemRef.current?.content === savedContent
737
755
  ? savedSystemRef.current.system
738
756
  : outsideSystem;
739
- const [draftSystem, setDraftSystem] = useState(savedSystem);
740
- // The draft is rebuilt on open and on a new value; so is its calendar. A new
741
- // calendarSystem prop while the user is picking starts the selection over,
742
- // since a selection is one calendar's.
743
- const systemSeen = useRef({ isOpen, valueKey, savedSystem });
744
- useEffect(() => {
745
- const seen = systemSeen.current;
746
- systemSeen.current = { isOpen, valueKey, savedSystem };
747
- setDraftSystem(savedSystem);
748
- const systemOnly =
749
- seen.isOpen === isOpen &&
750
- seen.valueKey === valueKey &&
751
- seen.savedSystem !== savedSystem;
752
- if (systemOnly && isOpen && edited.current) {
753
- draft.clear();
754
- resetSelectionUi();
755
- }
756
- // eslint-disable-next-line react-hooks/exhaustive-deps -- the draft's actions are stable
757
- }, [isOpen, valueKey, savedSystem]);
758
- useEffect(() => setDualTab(dualDefaultTab), [isOpen, dualDefaultTab]);
759
- const selectionShown = draftSystem === shownSystem;
757
+ const hasSaved = valuesOf(saved).some((v) => v !== null);
758
+ // Each open starts on the tab the saved value was saved with; with no value,
759
+ // on dualDefaultTab.
760
+ useEffect(
761
+ () => setDualTab(hasSaved ? savedSystem : dualDefaultTab),
762
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- on open and on a new dualDefaultTab, not on every save
763
+ [isOpen, dualDefaultTab]
764
+ );
760
765
  /** Saves a value: with the calendar it was picked in, when one can be Hijri. */
761
766
  const emitChange = useCallback(
762
767
  (wire: PickerValue, picked: 'gregory' | 'hijri') => {
@@ -794,13 +799,14 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
794
799
  !wheelSpinning &&
795
800
  !confirmDisabled;
796
801
 
797
- // Every save: Confirm, an 'instant' edit (onConfirm is added by `confirm`).
802
+ // Every save: Confirm, an 'instant' edit (onConfirm is added by `confirm`),
803
+ // with the calendar on screen.
798
804
  const commit = useCallback(
799
805
  (wire: PickerValue) => {
800
- emitChange(wire, draftSystem);
806
+ emitChange(wire, shownSystem);
801
807
  onBlur?.();
802
808
  },
803
- [emitChange, draftSystem, onBlur]
809
+ [emitChange, shownSystem, onBlur]
804
810
  );
805
811
 
806
812
  const confirm = useCallback(() => {
@@ -838,8 +844,8 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
838
844
  draft.clear();
839
845
  resetSelectionUi();
840
846
  edited.current = false;
841
- emitChange('', draftSystem);
842
- }, [draft, emitChange, draftSystem, resetSelectionUi]);
847
+ emitChange('', shownSystem);
848
+ }, [draft, emitChange, shownSystem, resetSelectionUi]);
843
849
 
844
850
  const clearDraft = useCallback(() => {
845
851
  edited.current = true;
@@ -849,7 +855,6 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
849
855
 
850
856
  // × in the field above the calendar. 'instant' saves every edit, so it
851
857
  // clears the saved value too (once; an empty value isn't saved again).
852
- const hasSaved = valuesOf(saved).some((v) => v !== null);
853
858
  const clearInSheet = useCallback(() => {
854
859
  if (commitMode === 'instant' && hasSaved) clearValue();
855
860
  else clearDraft();
@@ -862,7 +867,8 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
862
867
  // Confirm press, but only while open (closing discards, as with Confirm),
863
868
  // and only what the draft holds: a time still being typed isn't saved yet.
864
869
  // An edit equal to the saved value isn't saved again; one `validate`
865
- // rejected waits until the selection or the saved value changes.
870
+ // rejected waits until the selection or the saved value changes. A tab
871
+ // change the user makes saves the selection again with the new calendar.
866
872
  const lastRejected = useRef<string | null>(null);
867
873
  // The last value saved: not saved again while the parent's `value` catches
868
874
  // up (a slow store, a server round trip) or when it never does.
@@ -884,12 +890,13 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
884
890
  if (!typingDone || timeIssue || !canSave(draft.draft)) return;
885
891
  const wire = draft.toWire();
886
892
  const key = JSON.stringify(wire);
887
- if (key === lastRejected.current || key === lastEmitted.current) return;
888
- // The same dates picked in the same calendar aren't saved again; the same
889
- // dates picked in the other calendar are.
893
+ const sent = `${shownSystem} ${key}`;
894
+ if (key === lastRejected.current || sent === lastEmitted.current) return;
895
+ // The same dates in the same calendar aren't saved again; the same dates
896
+ // with the other tab on screen are.
890
897
  const unchanged =
891
898
  key === valueKey || contentOf(draft.draft) === savedContent;
892
- if (unchanged && draftSystem === savedSystem) return;
899
+ if (unchanged && shownSystem === savedSystem) return;
893
900
  const message = latest.current.validate?.(wire);
894
901
  if (message) {
895
902
  lastRejected.current = key;
@@ -897,7 +904,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
897
904
  latest.current.onError?.('VALIDATION', wire);
898
905
  return;
899
906
  }
900
- lastEmitted.current = key;
907
+ lastEmitted.current = sent;
901
908
  commit(wire);
902
909
  };
903
910
  // Only typing waits for the field to be left; a tap is a finished edit.
@@ -910,7 +917,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
910
917
  isOpen,
911
918
  sheetTyping,
912
919
  draft.draft,
913
- draftSystem,
920
+ shownSystem,
914
921
  timeIssue,
915
922
  typingBlocked,
916
923
  wheelSpinning,
@@ -928,7 +935,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
928
935
  }, [closePicker]);
929
936
 
930
937
  // A typed date with no time yet gets one the way a tapped day does: the fixed
931
- // newDayTime, else a time already typed in the time row, else now.
938
+ // newDayTime, else a time already typed in the time row, else now (or none).
932
939
  const withFilledTime = (
933
940
  parsed: ParsedSegments,
934
941
  key: 'single' | RangeEnd
@@ -937,20 +944,13 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
937
944
  (parsed.dateOnly
938
945
  ? {
939
946
  date: parsed.dateOnly,
940
- time: fixedTime ?? pending[key] ?? nowTime(),
947
+ time: fixedTime ?? pending[key] ?? newTime(),
941
948
  }
942
949
  : null);
943
950
 
944
- // Typed segments are Gregorian, so typing keeps the calendar a selection was
945
- // picked in; into an empty draft, it takes the calendar on screen.
946
- const adoptShownIfEmpty = () => {
947
- if (valuesOf(d).every((v) => v === null)) setDraftSystem(shownSystem);
948
- };
949
-
950
951
  const onSheetTyped = (parsed: ParsedSegments) => {
951
952
  edited.current = true;
952
953
  typedLast.current = true;
953
- adoptShownIfEmpty();
954
954
  const typed = withFilledTime(parsed, 'single');
955
955
  setTypingBlock('single', !parsed.isEmpty && !typed);
956
956
  if (typed) draft.setValue(typed, 'single');
@@ -960,14 +960,13 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
960
960
  const onRangeTyped = (end: RangeEnd, parsed: ParsedSegments) => {
961
961
  edited.current = true;
962
962
  typedLast.current = true;
963
- adoptShownIfEmpty();
964
963
  const typed = withFilledTime(parsed, end);
965
964
  setTypingBlock(end, !parsed.isEmpty && !typed);
966
965
  if (typed || parsed.isEmpty) draft.setValue(typed, end);
967
966
  };
968
967
 
969
968
  // A fixed time for every newly picked day (wins over a typed time and over "now").
970
- const fixedTime = newDayTime ?? null;
969
+ const fixedTime = typeof newDayTime === 'object' ? newDayTime : null;
971
970
 
972
971
  // A day picked on an empty date-time starts at the current time, moved up to
973
972
  // the next allowed minute step. An existing time is kept.
@@ -978,6 +977,11 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
978
977
  const capped = Math.min(minutes, 23 * 60 + 60 - step);
979
978
  return { hour: Math.floor(capped / 60), minute: capped % 60 };
980
979
  }, [now, zone, minuteStep]);
980
+ /** A new day's time when nothing fixed or typed: now, or none with newDayTime 'empty'. */
981
+ const newTime = useCallback(
982
+ (): Time | null => (startsEmpty ? null : nowTime()),
983
+ [startsEmpty, nowTime]
984
+ );
981
985
 
982
986
  // A range end picked on the start's day starts an hour after the start (at
983
987
  // most the day's latest allowed time, or its last step), so the range isn't
@@ -987,10 +991,10 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
987
991
  from: DateTimeValue | null,
988
992
  day: CalendarDate,
989
993
  typed: Time | null | undefined
990
- ): Time => {
994
+ ): Time | null => {
991
995
  if (!fixedTime && typed) return typed;
992
- const time = fixedTime ?? nowTime();
993
- if (!from?.time || !isSameDay(from.date, day)) return time;
996
+ const time = fixedTime ?? newTime();
997
+ if (!time || !from?.time || !isSameDay(from.date, day)) return time;
994
998
  const start = from.time.hour * 60 + from.time.minute;
995
999
  if (time.hour * 60 + time.minute > start) return time;
996
1000
  // Within the day's allowed hours when it has them.
@@ -1006,99 +1010,87 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1006
1010
  (day: CalendarDate) => {
1007
1011
  edited.current = true;
1008
1012
  typedLast.current = false;
1009
- // A pick in the other calendar starts over: the selection is one calendar's.
1010
- const fresh =
1011
- draftSystem !== shownSystem && valuesOf(d).some((v) => v !== null);
1012
- setDraftSystem(shownSystem);
1013
- const cur = fresh ? emptied(d) : d;
1014
- const end0: RangeEnd = fresh ? 'from' : activeEnd;
1015
- const dayInEdit = fresh ? null : activeDay;
1016
- const times: PendingTimes = fresh ? {} : pending;
1017
- if (fresh) {
1018
- draft.clear();
1019
- resetSelectionUi();
1020
- }
1021
- if (cur.mode === 'range') {
1013
+ // A multiple picker scrolls for its first date only, so more can be picked.
1014
+ if (d.mode !== 'multiple' || d.slots.length === 0) revealTime();
1015
+ if (d.mode === 'range') {
1022
1016
  if (rangeLayout === 'fromTo') {
1023
1017
  setEndPicked(false);
1024
1018
  // A full range: the tap starts a new one (or moves the nearest end).
1025
- if (!endPicked && cur.from.value && cur.to.value) {
1019
+ if (!endPicked && d.from.value && d.to.value) {
1026
1020
  const moveNearest = range?.reselect === 'moveNearestEnd';
1027
1021
  const nearest: RangeEnd =
1028
- Math.abs(daysBetween(cur.from.value.date, day)) <
1029
- Math.abs(daysBetween(cur.to.value.date, day))
1022
+ Math.abs(daysBetween(d.from.value.date, day)) <
1023
+ Math.abs(daysBetween(d.to.value.date, day))
1030
1024
  ? 'from'
1031
1025
  : 'to';
1032
1026
  const end = moveNearest ? nearest : 'from';
1033
1027
  setTypingBlock('from', false);
1034
1028
  setTypingBlock('to', false);
1035
- draft.selectDay(day, fixedTime ?? times[end] ?? nowTime());
1029
+ draft.selectDay(day, fixedTime ?? pending[end] ?? newTime());
1036
1030
  setActiveEnd(moveNearest ? nearest : 'to');
1037
1031
  return;
1038
1032
  }
1039
1033
  // Choosing the end after a start tap: a day before the start moves the start.
1040
1034
  const end1: RangeEnd =
1041
- end0 === 'to' &&
1035
+ activeEnd === 'to' &&
1042
1036
  !endPicked &&
1043
- cur.from.value &&
1044
- compareCalendarDates(day, cur.from.value.date) < 0
1037
+ d.from.value &&
1038
+ compareCalendarDates(day, d.from.value.date) < 0
1045
1039
  ? 'from'
1046
- : end0;
1040
+ : activeEnd;
1047
1041
  // The tapped day replaces whatever was typed in this end only.
1048
1042
  setTypingBlock(end1, false);
1049
1043
  draft.setRangeEnd(
1050
1044
  day,
1051
1045
  end1,
1052
1046
  end1 === 'to'
1053
- ? endTimeFor(cur.from.value, day, times.to)
1054
- : (fixedTime ?? times.from ?? nowTime())
1047
+ ? endTimeFor(d.from.value, day, pending.to)
1048
+ : (fixedTime ?? pending.from ?? newTime())
1055
1049
  );
1056
1050
  // The start tapped again where a range can't be one day deselects
1057
1051
  // it, so the next tap picks the start again.
1058
1052
  const deselects =
1059
1053
  end1 === 'to' &&
1060
1054
  !sameDayRangeOk(draftOptions) &&
1061
- isSameDay(cur.from.value?.date, day);
1062
- if (end0 === 'from') setActiveEnd('to');
1055
+ isSameDay(d.from.value?.date, day);
1056
+ if (activeEnd === 'from') setActiveEnd('to');
1063
1057
  else if (deselects) setActiveEnd('from');
1064
1058
  return;
1065
1059
  }
1066
1060
  // The single field: the first tap is the start, the next one the end.
1067
1061
  const startsOver =
1068
- !cur.from.value ||
1069
- (!!cur.to.value && range?.reselect !== 'moveNearestEnd');
1062
+ !d.from.value ||
1063
+ (!!d.to.value && range?.reselect !== 'moveNearestEnd');
1070
1064
  const end: RangeEnd = startsOver ? 'from' : 'to';
1071
1065
  setTypingBlock('single', false);
1072
1066
  draft.selectDay(
1073
1067
  day,
1074
- end === 'to' && !cur.to.value
1075
- ? endTimeFor(cur.from.value, day, times.to)
1076
- : (fixedTime ?? times[end] ?? nowTime())
1068
+ end === 'to' && !d.to.value
1069
+ ? endTimeFor(d.from.value, day, pending.to)
1070
+ : (fixedTime ?? pending[end] ?? newTime())
1077
1071
  );
1078
1072
  return;
1079
1073
  }
1080
- if (cur.mode === 'multiple') {
1074
+ if (d.mode === 'multiple') {
1081
1075
  const perDate = type === 'datetime' && multipleTimeMode === 'perDate';
1082
- const picked = cur.slots.some((slot) =>
1083
- isSameDay(slot.value?.date, day)
1084
- );
1076
+ const picked = d.slots.some((slot) => isSameDay(slot.value?.date, day));
1085
1077
  // perDate: a tap on a picked day first makes it the one the time row edits.
1086
- if (perDate && picked && !isSameDay(dayInEdit, day)) {
1078
+ if (perDate && picked && !isSameDay(activeDay, day)) {
1087
1079
  setActiveDay(day);
1088
1080
  return;
1089
1081
  }
1090
- draft.selectDay(day, fixedTime ?? nowTime());
1082
+ draft.selectDay(day, fixedTime ?? newTime());
1091
1083
  if (perDate) setActiveDay(picked ? null : day);
1092
1084
  return;
1093
1085
  }
1094
1086
  setTypingBlock('single', false);
1095
- draft.selectDay(day, fixedTime ?? times.single ?? nowTime());
1087
+ draft.selectDay(day, fixedTime ?? pending.single ?? newTime());
1096
1088
  },
1097
1089
  // eslint-disable-next-line react-hooks/exhaustive-deps -- valuesOf reads only `d` and props listed here
1098
1090
  [
1099
1091
  d,
1100
1092
  draft,
1101
- nowTime,
1093
+ newTime,
1102
1094
  rangeLayout,
1103
1095
  activeEnd,
1104
1096
  activeDay,
@@ -1108,13 +1100,11 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1108
1100
  type,
1109
1101
  multipleTimeMode,
1110
1102
  setTypingBlock,
1111
- draftSystem,
1112
- shownSystem,
1113
- resetSelectionUi,
1114
1103
  endPicked,
1115
1104
  draftOptions,
1116
1105
  minuteStep,
1117
1106
  constraints,
1107
+ revealTime,
1118
1108
  ]
1119
1109
  );
1120
1110
 
@@ -1271,12 +1261,13 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1271
1261
  ? (v) =>
1272
1262
  hijriDateText(v.date, hijriLocale) ?? formatDateText(v, dateFormat)
1273
1263
  : undefined;
1274
- const savedDateText = dateTextFor(savedSystem);
1275
- const draftDateText = dateTextFor(draftSystem);
1276
- const savedStrings = stringsFor(savedSystem);
1277
- const draftStrings = stringsFor(draftSystem);
1278
- // Error messages inside the picker follow the tab on screen; the on-screen
1279
- // field's keep `strings`.
1264
+ // The calendar the on-screen field writes the saved value in.
1265
+ const fieldSystem = fieldCalendar === 'gregory' ? 'gregory' : savedSystem;
1266
+ const savedDateText = dateTextFor(fieldSystem);
1267
+ const draftDateText = dateTextFor(shownSystem);
1268
+ const savedStrings = stringsFor(fieldSystem);
1269
+ // The texts inside the picker, its error messages too, follow the tab on
1270
+ // screen; the on-screen field's keep `strings`.
1280
1271
  const shownStrings = stringsFor(shownSystem);
1281
1272
 
1282
1273
  // What the on-screen field shows: the saved value, never the draft.
@@ -1353,7 +1344,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1353
1344
  type,
1354
1345
  mode,
1355
1346
  value,
1356
- displayText: digitsFor(savedSystem)(
1347
+ displayText: digitsFor(fieldSystem)(
1357
1348
  savedText ??
1358
1349
  formatValueText(
1359
1350
  savedSingle,
@@ -1388,8 +1379,12 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1388
1379
  const sheetValue =
1389
1380
  draft.draft.mode === 'single' ? draft.draft.slot.value : null;
1390
1381
  // A time outside the limits is explained under the time row, not here.
1382
+ const notAllowedError =
1383
+ sheetOptions?.showNotAllowedError === false
1384
+ ? ''
1385
+ : shownStrings.errorNotAllowed;
1391
1386
  const sheetValueError =
1392
- sheetValue && dayOff(sheetValue.date) ? shownStrings.errorNotAllowed : '';
1387
+ sheetValue && dayOff(sheetValue.date) ? notAllowedError : '';
1393
1388
 
1394
1389
  let rangeError = '';
1395
1390
  if (d.mode === 'range' && d.from.value && d.to.value) {
@@ -1398,7 +1393,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1398
1393
  constraints.dayStatus(d.from.value.date).disabled ||
1399
1394
  constraints.dayStatus(d.to.value.date).disabled
1400
1395
  )
1401
- rangeError = shownStrings.errorNotAllowed;
1396
+ rangeError = notAllowedError;
1402
1397
  else if (previewIssue === 'RANGE_ORDER')
1403
1398
  rangeError = shownStrings.errorRangeOrder;
1404
1399
  else if (previewIssue === 'RANGE_LENGTH')
@@ -1473,7 +1468,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1473
1468
  ) : mode === 'single' ? (
1474
1469
  <ValueField
1475
1470
  {...fieldCommon}
1476
- strings={draftStrings}
1471
+ strings={shownStrings}
1477
1472
  errorStrings={shownStrings}
1478
1473
  value={sheetValue}
1479
1474
  displayText={precision !== 'day' ? periodText(sheetValue) : undefined}
@@ -1492,7 +1487,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1492
1487
  ) : d.mode === 'range' ? (
1493
1488
  <RangeFields
1494
1489
  {...fieldCommon}
1495
- strings={draftStrings}
1490
+ strings={shownStrings}
1496
1491
  errorStrings={shownStrings}
1497
1492
  layout={rangeLayout}
1498
1493
  dateText={draftDateText}
@@ -1514,7 +1509,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1514
1509
  ) : d.mode === 'multiple' ? (
1515
1510
  <ValueField
1516
1511
  {...fieldCommon}
1517
- strings={draftStrings}
1512
+ strings={shownStrings}
1518
1513
  value={null}
1519
1514
  displayText={formatMultipleText(
1520
1515
  draftValues,
@@ -1522,7 +1517,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1522
1517
  dateFormat,
1523
1518
  timeFormat,
1524
1519
  draftDateText,
1525
- draftStrings
1520
+ shownStrings
1526
1521
  )}
1527
1522
  placeholder={sheetOptions?.fieldPlaceholder}
1528
1523
  allowClear={allowClear}
@@ -1541,13 +1536,20 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1541
1536
  };
1542
1537
  const systemA11yLabel =
1543
1538
  strings.a11yCalendarSystem ?? DEFAULT_STRINGS.a11yCalendarSystem;
1539
+ // A calendar or tab the user picks is an edit: 'instant' saves the selection
1540
+ // again with that calendar.
1544
1541
  const chooseSystem = (next: CalendarSystem) => {
1545
1542
  if (next === system) return;
1543
+ edited.current = true;
1546
1544
  setChosenSystem(next);
1547
1545
  if (next === 'dual') setDualTab(dualDefaultTab);
1548
1546
  echo.current = next;
1549
1547
  onCalendarSystemChange?.(next);
1550
1548
  };
1549
+ const chooseTab = (next: 'gregory' | 'hijri') => {
1550
+ edited.current = true;
1551
+ setDualTab(next);
1552
+ };
1551
1553
  const calendarSwitchNode = !switchSystems ? null : renderCalendarSwitch ? (
1552
1554
  renderCalendarSwitch({
1553
1555
  systems: switchSystems,
@@ -1579,7 +1581,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1579
1581
  { value: 'hijri', label: systemLabels.hijri },
1580
1582
  ]}
1581
1583
  selected={dualTab}
1582
- onSelect={setDualTab}
1584
+ onSelect={chooseTab}
1583
1585
  accessibilityLabel={systemA11yLabel}
1584
1586
  tokens={tokens}
1585
1587
  styles={styles}
@@ -1595,12 +1597,9 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1595
1597
  <View style={hijriShown ? RTL : undefined}>{node}</View>
1596
1598
  </RtlContext.Provider>
1597
1599
  );
1598
- // Another calendar opens on the days the last one showed, else on the
1599
- // selection (shown or not), else where the picker would open.
1600
- const calendarAnchor =
1601
- shownAnchor.current ??
1602
- (selectionShown ? null : firstSelectedDay(selection)) ??
1603
- initialVisibleDate;
1600
+ // Another calendar opens on the days the last one showed, else where the
1601
+ // picker would open.
1602
+ const calendarAnchor = shownAnchor.current ?? initialVisibleDate;
1604
1603
 
1605
1604
  const calendar = (
1606
1605
  <View style={folded ? FOLDED : undefined}>
@@ -1609,10 +1608,10 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1609
1608
  {mirrored(
1610
1609
  <Calendar
1611
1610
  key={shownSystem}
1612
- constraints={selectionShown ? calendarConstraints : constraints}
1611
+ constraints={calendarConstraints}
1613
1612
  today={today}
1614
1613
  initialVisibleDate={calendarAnchor}
1615
- selection={selectionShown ? selection : emptySelection(selection)}
1614
+ selection={selection}
1616
1615
  onSelectDay={onSelectDayStable}
1617
1616
  firstDayOfWeek={firstDayOfWeek}
1618
1617
  showOutsideDays={showOutsideDays}
@@ -1688,19 +1687,25 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1688
1687
  ? validation.message
1689
1688
  : null;
1690
1689
 
1691
- // The field writes the date in the digits of the calendar it was picked in;
1692
- // the calendar and the time rows follow the tab that shows.
1690
+ // The field, the calendar and the time rows follow the tab that shows.
1693
1691
  const sheetContent = (
1694
1692
  <>
1695
- <NumeralsContext.Provider value={digitsFor(draftSystem)}>
1696
- {sheetField}
1697
- </NumeralsContext.Provider>
1698
1693
  <NumeralsContext.Provider value={digitsFor(shownSystem)}>
1694
+ {sheetField}
1699
1695
  {calendar}
1700
1696
  </NumeralsContext.Provider>
1701
- <NumeralsContext.Provider value={digitsFor(shownSystem)}>
1702
- {mirrored(timeRow)}
1703
- </NumeralsContext.Provider>
1697
+ {timeRow == null ? null : (
1698
+ // Measured for the scroll to the time (sheet.scrollToTime).
1699
+ <View
1700
+ ref={timeRef}
1701
+ collapsable={false}
1702
+ testID={testID ? `${testID}-time-section` : undefined}
1703
+ >
1704
+ <NumeralsContext.Provider value={digitsFor(shownSystem)}>
1705
+ {mirrored(timeRow)}
1706
+ </NumeralsContext.Provider>
1707
+ </View>
1708
+ )}
1704
1709
  {validationMessage ? (
1705
1710
  <FieldMessages
1706
1711
  error={validationMessage}
@@ -1716,7 +1721,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1716
1721
  );
1717
1722
 
1718
1723
  return (
1719
- <NumeralsContext.Provider value={digitsFor(savedSystem)}>
1724
+ <NumeralsContext.Provider value={digitsFor(fieldSystem)}>
1720
1725
  {trigger}
1721
1726
  <PickerContainer
1722
1727
  presentation={presentation}
@@ -1724,6 +1729,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1724
1729
  onRequestClose={close}
1725
1730
  renderSheet={renderSheet}
1726
1731
  sheet={sheetOptions}
1732
+ scrollRef={scrollRef}
1727
1733
  state={state}
1728
1734
  header={header}
1729
1735
  title={title ?? (isInline ? undefined : strings.titleDate)}