rn-grid-date-time-picker 1.3.0 → 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 (57) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/lib/commonjs/components/DateTimePicker.js +67 -115
  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/field/ValueField.js +19 -2
  9. package/lib/commonjs/components/field/ValueField.js.map +1 -1
  10. package/lib/commonjs/components/field/fieldText.js +44 -5
  11. package/lib/commonjs/components/field/fieldText.js.map +1 -1
  12. package/lib/commonjs/components/theme/styleKeys.js +3 -1
  13. package/lib/commonjs/components/theme/styleKeys.js.map +1 -1
  14. package/lib/commonjs/components/types.js +2 -1
  15. package/lib/commonjs/components/types.js.map +1 -1
  16. package/lib/module/components/DateTimePicker.js +68 -116
  17. package/lib/module/components/DateTimePicker.js.map +1 -1
  18. package/lib/module/components/calendar/DayCell.js +18 -7
  19. package/lib/module/components/calendar/DayCell.js.map +1 -1
  20. package/lib/module/components/calendar/calendarStyles.js +11 -0
  21. package/lib/module/components/calendar/calendarStyles.js.map +1 -1
  22. package/lib/module/components/field/ValueField.js +20 -3
  23. package/lib/module/components/field/ValueField.js.map +1 -1
  24. package/lib/module/components/field/fieldText.js +40 -4
  25. package/lib/module/components/field/fieldText.js.map +1 -1
  26. package/lib/module/components/theme/styleKeys.js +3 -1
  27. package/lib/module/components/theme/styleKeys.js.map +1 -1
  28. package/lib/module/components/types.js +2 -1
  29. package/lib/module/components/types.js.map +1 -1
  30. package/lib/typescript/commonjs/src/components/DateTimePicker.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/src/components/calendar/DayCell.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/src/components/calendar/calendarStyles.d.ts +9 -0
  33. package/lib/typescript/commonjs/src/components/calendar/calendarStyles.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/src/components/field/ValueField.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/src/components/field/fieldText.d.ts +19 -0
  36. package/lib/typescript/commonjs/src/components/field/fieldText.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/src/components/theme/styleKeys.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/src/components/types.d.ts +18 -5
  39. package/lib/typescript/commonjs/src/components/types.d.ts.map +1 -1
  40. package/lib/typescript/module/src/components/DateTimePicker.d.ts.map +1 -1
  41. package/lib/typescript/module/src/components/calendar/DayCell.d.ts.map +1 -1
  42. package/lib/typescript/module/src/components/calendar/calendarStyles.d.ts +9 -0
  43. package/lib/typescript/module/src/components/calendar/calendarStyles.d.ts.map +1 -1
  44. package/lib/typescript/module/src/components/field/ValueField.d.ts.map +1 -1
  45. package/lib/typescript/module/src/components/field/fieldText.d.ts +19 -0
  46. package/lib/typescript/module/src/components/field/fieldText.d.ts.map +1 -1
  47. package/lib/typescript/module/src/components/theme/styleKeys.d.ts.map +1 -1
  48. package/lib/typescript/module/src/components/types.d.ts +18 -5
  49. package/lib/typescript/module/src/components/types.d.ts.map +1 -1
  50. package/package.json +1 -1
  51. package/src/components/DateTimePicker.tsx +79 -120
  52. package/src/components/calendar/DayCell.tsx +25 -6
  53. package/src/components/calendar/calendarStyles.ts +11 -0
  54. package/src/components/field/ValueField.tsx +30 -11
  55. package/src/components/field/fieldText.ts +48 -5
  56. package/src/components/theme/styleKeys.ts +3 -1
  57. package/src/components/types.ts +18 -5
@@ -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,
@@ -95,14 +94,6 @@ const sameValue = (a: DateTimeValue, b: DateTimeValue): boolean =>
95
94
  const ALL_SYSTEMS: CalendarSystem[] = ['gregory', 'hijri', 'dual'];
96
95
  const RTL = { direction: 'rtl' } as const;
97
96
 
98
- /** The same kind of selection with nothing in it. */
99
- const emptySelection = (sel: CalendarSelection): CalendarSelection =>
100
- sel.kind === 'single'
101
- ? { kind: 'single', day: null }
102
- : sel.kind === 'range'
103
- ? { kind: 'range', from: null, to: null }
104
- : { ...sel, days: [] };
105
-
106
97
  /** Minute steps that divide the hour. */
107
98
  const MINUTE_STEPS = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60];
108
99
 
@@ -741,10 +732,10 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
741
732
  latest.current = { validate, onError, value };
742
733
  const valueKey = JSON.stringify(value ?? '');
743
734
 
744
- // A selection belongs to the calendar it was picked in (Gregorian or Hijri):
745
- // it shows only there, and the field writes it in that calendar. A saved
746
- // value remembers its calendar; one from outside counts as picked in the
747
- // 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.
748
739
  // The saved value as a draft, for the on-screen field and for comparing
749
740
  // values by what they hold.
750
741
  const saved = useMemo(
@@ -763,27 +754,14 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
763
754
  savedSystemRef.current?.content === savedContent
764
755
  ? savedSystemRef.current.system
765
756
  : outsideSystem;
766
- const [draftSystem, setDraftSystem] = useState(savedSystem);
767
- // The draft is rebuilt on open and on a new value; so is its calendar. A new
768
- // calendarSystem prop while the user is picking starts the selection over,
769
- // since a selection is one calendar's.
770
- const systemSeen = useRef({ isOpen, valueKey, savedSystem });
771
- useEffect(() => {
772
- const seen = systemSeen.current;
773
- systemSeen.current = { isOpen, valueKey, savedSystem };
774
- setDraftSystem(savedSystem);
775
- const systemOnly =
776
- seen.isOpen === isOpen &&
777
- seen.valueKey === valueKey &&
778
- seen.savedSystem !== savedSystem;
779
- if (systemOnly && isOpen && edited.current) {
780
- draft.clear();
781
- resetSelectionUi();
782
- }
783
- // eslint-disable-next-line react-hooks/exhaustive-deps -- the draft's actions are stable
784
- }, [isOpen, valueKey, savedSystem]);
785
- useEffect(() => setDualTab(dualDefaultTab), [isOpen, dualDefaultTab]);
786
- 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
+ );
787
765
  /** Saves a value: with the calendar it was picked in, when one can be Hijri. */
788
766
  const emitChange = useCallback(
789
767
  (wire: PickerValue, picked: 'gregory' | 'hijri') => {
@@ -821,13 +799,14 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
821
799
  !wheelSpinning &&
822
800
  !confirmDisabled;
823
801
 
824
- // 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.
825
804
  const commit = useCallback(
826
805
  (wire: PickerValue) => {
827
- emitChange(wire, draftSystem);
806
+ emitChange(wire, shownSystem);
828
807
  onBlur?.();
829
808
  },
830
- [emitChange, draftSystem, onBlur]
809
+ [emitChange, shownSystem, onBlur]
831
810
  );
832
811
 
833
812
  const confirm = useCallback(() => {
@@ -865,8 +844,8 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
865
844
  draft.clear();
866
845
  resetSelectionUi();
867
846
  edited.current = false;
868
- emitChange('', draftSystem);
869
- }, [draft, emitChange, draftSystem, resetSelectionUi]);
847
+ emitChange('', shownSystem);
848
+ }, [draft, emitChange, shownSystem, resetSelectionUi]);
870
849
 
871
850
  const clearDraft = useCallback(() => {
872
851
  edited.current = true;
@@ -876,7 +855,6 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
876
855
 
877
856
  // × in the field above the calendar. 'instant' saves every edit, so it
878
857
  // clears the saved value too (once; an empty value isn't saved again).
879
- const hasSaved = valuesOf(saved).some((v) => v !== null);
880
858
  const clearInSheet = useCallback(() => {
881
859
  if (commitMode === 'instant' && hasSaved) clearValue();
882
860
  else clearDraft();
@@ -889,7 +867,8 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
889
867
  // Confirm press, but only while open (closing discards, as with Confirm),
890
868
  // and only what the draft holds: a time still being typed isn't saved yet.
891
869
  // An edit equal to the saved value isn't saved again; one `validate`
892
- // 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.
893
872
  const lastRejected = useRef<string | null>(null);
894
873
  // The last value saved: not saved again while the parent's `value` catches
895
874
  // up (a slow store, a server round trip) or when it never does.
@@ -911,12 +890,13 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
911
890
  if (!typingDone || timeIssue || !canSave(draft.draft)) return;
912
891
  const wire = draft.toWire();
913
892
  const key = JSON.stringify(wire);
914
- if (key === lastRejected.current || key === lastEmitted.current) return;
915
- // The same dates picked in the same calendar aren't saved again; the same
916
- // 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.
917
897
  const unchanged =
918
898
  key === valueKey || contentOf(draft.draft) === savedContent;
919
- if (unchanged && draftSystem === savedSystem) return;
899
+ if (unchanged && shownSystem === savedSystem) return;
920
900
  const message = latest.current.validate?.(wire);
921
901
  if (message) {
922
902
  lastRejected.current = key;
@@ -924,7 +904,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
924
904
  latest.current.onError?.('VALIDATION', wire);
925
905
  return;
926
906
  }
927
- lastEmitted.current = key;
907
+ lastEmitted.current = sent;
928
908
  commit(wire);
929
909
  };
930
910
  // Only typing waits for the field to be left; a tap is a finished edit.
@@ -937,7 +917,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
937
917
  isOpen,
938
918
  sheetTyping,
939
919
  draft.draft,
940
- draftSystem,
920
+ shownSystem,
941
921
  timeIssue,
942
922
  typingBlocked,
943
923
  wheelSpinning,
@@ -968,16 +948,9 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
968
948
  }
969
949
  : null);
970
950
 
971
- // Typed segments are Gregorian, so typing keeps the calendar a selection was
972
- // picked in; into an empty draft, it takes the calendar on screen.
973
- const adoptShownIfEmpty = () => {
974
- if (valuesOf(d).every((v) => v === null)) setDraftSystem(shownSystem);
975
- };
976
-
977
951
  const onSheetTyped = (parsed: ParsedSegments) => {
978
952
  edited.current = true;
979
953
  typedLast.current = true;
980
- adoptShownIfEmpty();
981
954
  const typed = withFilledTime(parsed, 'single');
982
955
  setTypingBlock('single', !parsed.isEmpty && !typed);
983
956
  if (typed) draft.setValue(typed, 'single');
@@ -987,7 +960,6 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
987
960
  const onRangeTyped = (end: RangeEnd, parsed: ParsedSegments) => {
988
961
  edited.current = true;
989
962
  typedLast.current = true;
990
- adoptShownIfEmpty();
991
963
  const typed = withFilledTime(parsed, end);
992
964
  setTypingBlock(end, !parsed.isEmpty && !typed);
993
965
  if (typed || parsed.isEmpty) draft.setValue(typed, end);
@@ -1038,86 +1010,72 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1038
1010
  (day: CalendarDate) => {
1039
1011
  edited.current = true;
1040
1012
  typedLast.current = false;
1041
- // A pick in the other calendar starts over: the selection is one calendar's.
1042
- const fresh =
1043
- draftSystem !== shownSystem && valuesOf(d).some((v) => v !== null);
1044
- setDraftSystem(shownSystem);
1045
- const cur = fresh ? emptied(d) : d;
1046
- const end0: RangeEnd = fresh ? 'from' : activeEnd;
1047
- const dayInEdit = fresh ? null : activeDay;
1048
- const times: PendingTimes = fresh ? {} : pending;
1049
- if (fresh) {
1050
- draft.clear();
1051
- resetSelectionUi();
1052
- }
1053
1013
  // A multiple picker scrolls for its first date only, so more can be picked.
1054
- if (cur.mode !== 'multiple' || cur.slots.length === 0) revealTime();
1055
- if (cur.mode === 'range') {
1014
+ if (d.mode !== 'multiple' || d.slots.length === 0) revealTime();
1015
+ if (d.mode === 'range') {
1056
1016
  if (rangeLayout === 'fromTo') {
1057
1017
  setEndPicked(false);
1058
1018
  // A full range: the tap starts a new one (or moves the nearest end).
1059
- if (!endPicked && cur.from.value && cur.to.value) {
1019
+ if (!endPicked && d.from.value && d.to.value) {
1060
1020
  const moveNearest = range?.reselect === 'moveNearestEnd';
1061
1021
  const nearest: RangeEnd =
1062
- Math.abs(daysBetween(cur.from.value.date, day)) <
1063
- 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))
1064
1024
  ? 'from'
1065
1025
  : 'to';
1066
1026
  const end = moveNearest ? nearest : 'from';
1067
1027
  setTypingBlock('from', false);
1068
1028
  setTypingBlock('to', false);
1069
- draft.selectDay(day, fixedTime ?? times[end] ?? newTime());
1029
+ draft.selectDay(day, fixedTime ?? pending[end] ?? newTime());
1070
1030
  setActiveEnd(moveNearest ? nearest : 'to');
1071
1031
  return;
1072
1032
  }
1073
1033
  // Choosing the end after a start tap: a day before the start moves the start.
1074
1034
  const end1: RangeEnd =
1075
- end0 === 'to' &&
1035
+ activeEnd === 'to' &&
1076
1036
  !endPicked &&
1077
- cur.from.value &&
1078
- compareCalendarDates(day, cur.from.value.date) < 0
1037
+ d.from.value &&
1038
+ compareCalendarDates(day, d.from.value.date) < 0
1079
1039
  ? 'from'
1080
- : end0;
1040
+ : activeEnd;
1081
1041
  // The tapped day replaces whatever was typed in this end only.
1082
1042
  setTypingBlock(end1, false);
1083
1043
  draft.setRangeEnd(
1084
1044
  day,
1085
1045
  end1,
1086
1046
  end1 === 'to'
1087
- ? endTimeFor(cur.from.value, day, times.to)
1088
- : (fixedTime ?? times.from ?? newTime())
1047
+ ? endTimeFor(d.from.value, day, pending.to)
1048
+ : (fixedTime ?? pending.from ?? newTime())
1089
1049
  );
1090
1050
  // The start tapped again where a range can't be one day deselects
1091
1051
  // it, so the next tap picks the start again.
1092
1052
  const deselects =
1093
1053
  end1 === 'to' &&
1094
1054
  !sameDayRangeOk(draftOptions) &&
1095
- isSameDay(cur.from.value?.date, day);
1096
- if (end0 === 'from') setActiveEnd('to');
1055
+ isSameDay(d.from.value?.date, day);
1056
+ if (activeEnd === 'from') setActiveEnd('to');
1097
1057
  else if (deselects) setActiveEnd('from');
1098
1058
  return;
1099
1059
  }
1100
1060
  // The single field: the first tap is the start, the next one the end.
1101
1061
  const startsOver =
1102
- !cur.from.value ||
1103
- (!!cur.to.value && range?.reselect !== 'moveNearestEnd');
1062
+ !d.from.value ||
1063
+ (!!d.to.value && range?.reselect !== 'moveNearestEnd');
1104
1064
  const end: RangeEnd = startsOver ? 'from' : 'to';
1105
1065
  setTypingBlock('single', false);
1106
1066
  draft.selectDay(
1107
1067
  day,
1108
- end === 'to' && !cur.to.value
1109
- ? endTimeFor(cur.from.value, day, times.to)
1110
- : (fixedTime ?? times[end] ?? newTime())
1068
+ end === 'to' && !d.to.value
1069
+ ? endTimeFor(d.from.value, day, pending.to)
1070
+ : (fixedTime ?? pending[end] ?? newTime())
1111
1071
  );
1112
1072
  return;
1113
1073
  }
1114
- if (cur.mode === 'multiple') {
1074
+ if (d.mode === 'multiple') {
1115
1075
  const perDate = type === 'datetime' && multipleTimeMode === 'perDate';
1116
- const picked = cur.slots.some((slot) =>
1117
- isSameDay(slot.value?.date, day)
1118
- );
1076
+ const picked = d.slots.some((slot) => isSameDay(slot.value?.date, day));
1119
1077
  // perDate: a tap on a picked day first makes it the one the time row edits.
1120
- if (perDate && picked && !isSameDay(dayInEdit, day)) {
1078
+ if (perDate && picked && !isSameDay(activeDay, day)) {
1121
1079
  setActiveDay(day);
1122
1080
  return;
1123
1081
  }
@@ -1126,7 +1084,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1126
1084
  return;
1127
1085
  }
1128
1086
  setTypingBlock('single', false);
1129
- draft.selectDay(day, fixedTime ?? times.single ?? newTime());
1087
+ draft.selectDay(day, fixedTime ?? pending.single ?? newTime());
1130
1088
  },
1131
1089
  // eslint-disable-next-line react-hooks/exhaustive-deps -- valuesOf reads only `d` and props listed here
1132
1090
  [
@@ -1142,9 +1100,6 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1142
1100
  type,
1143
1101
  multipleTimeMode,
1144
1102
  setTypingBlock,
1145
- draftSystem,
1146
- shownSystem,
1147
- resetSelectionUi,
1148
1103
  endPicked,
1149
1104
  draftOptions,
1150
1105
  minuteStep,
@@ -1309,11 +1264,10 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1309
1264
  // The calendar the on-screen field writes the saved value in.
1310
1265
  const fieldSystem = fieldCalendar === 'gregory' ? 'gregory' : savedSystem;
1311
1266
  const savedDateText = dateTextFor(fieldSystem);
1312
- const draftDateText = dateTextFor(draftSystem);
1267
+ const draftDateText = dateTextFor(shownSystem);
1313
1268
  const savedStrings = stringsFor(fieldSystem);
1314
- const draftStrings = stringsFor(draftSystem);
1315
- // Error messages inside the picker follow the tab on screen; the on-screen
1316
- // field's keep `strings`.
1269
+ // The texts inside the picker, its error messages too, follow the tab on
1270
+ // screen; the on-screen field's keep `strings`.
1317
1271
  const shownStrings = stringsFor(shownSystem);
1318
1272
 
1319
1273
  // What the on-screen field shows: the saved value, never the draft.
@@ -1425,8 +1379,12 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1425
1379
  const sheetValue =
1426
1380
  draft.draft.mode === 'single' ? draft.draft.slot.value : null;
1427
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;
1428
1386
  const sheetValueError =
1429
- sheetValue && dayOff(sheetValue.date) ? shownStrings.errorNotAllowed : '';
1387
+ sheetValue && dayOff(sheetValue.date) ? notAllowedError : '';
1430
1388
 
1431
1389
  let rangeError = '';
1432
1390
  if (d.mode === 'range' && d.from.value && d.to.value) {
@@ -1435,7 +1393,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1435
1393
  constraints.dayStatus(d.from.value.date).disabled ||
1436
1394
  constraints.dayStatus(d.to.value.date).disabled
1437
1395
  )
1438
- rangeError = shownStrings.errorNotAllowed;
1396
+ rangeError = notAllowedError;
1439
1397
  else if (previewIssue === 'RANGE_ORDER')
1440
1398
  rangeError = shownStrings.errorRangeOrder;
1441
1399
  else if (previewIssue === 'RANGE_LENGTH')
@@ -1510,7 +1468,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1510
1468
  ) : mode === 'single' ? (
1511
1469
  <ValueField
1512
1470
  {...fieldCommon}
1513
- strings={draftStrings}
1471
+ strings={shownStrings}
1514
1472
  errorStrings={shownStrings}
1515
1473
  value={sheetValue}
1516
1474
  displayText={precision !== 'day' ? periodText(sheetValue) : undefined}
@@ -1529,7 +1487,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1529
1487
  ) : d.mode === 'range' ? (
1530
1488
  <RangeFields
1531
1489
  {...fieldCommon}
1532
- strings={draftStrings}
1490
+ strings={shownStrings}
1533
1491
  errorStrings={shownStrings}
1534
1492
  layout={rangeLayout}
1535
1493
  dateText={draftDateText}
@@ -1551,7 +1509,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1551
1509
  ) : d.mode === 'multiple' ? (
1552
1510
  <ValueField
1553
1511
  {...fieldCommon}
1554
- strings={draftStrings}
1512
+ strings={shownStrings}
1555
1513
  value={null}
1556
1514
  displayText={formatMultipleText(
1557
1515
  draftValues,
@@ -1559,7 +1517,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1559
1517
  dateFormat,
1560
1518
  timeFormat,
1561
1519
  draftDateText,
1562
- draftStrings
1520
+ shownStrings
1563
1521
  )}
1564
1522
  placeholder={sheetOptions?.fieldPlaceholder}
1565
1523
  allowClear={allowClear}
@@ -1578,13 +1536,20 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1578
1536
  };
1579
1537
  const systemA11yLabel =
1580
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.
1581
1541
  const chooseSystem = (next: CalendarSystem) => {
1582
1542
  if (next === system) return;
1543
+ edited.current = true;
1583
1544
  setChosenSystem(next);
1584
1545
  if (next === 'dual') setDualTab(dualDefaultTab);
1585
1546
  echo.current = next;
1586
1547
  onCalendarSystemChange?.(next);
1587
1548
  };
1549
+ const chooseTab = (next: 'gregory' | 'hijri') => {
1550
+ edited.current = true;
1551
+ setDualTab(next);
1552
+ };
1588
1553
  const calendarSwitchNode = !switchSystems ? null : renderCalendarSwitch ? (
1589
1554
  renderCalendarSwitch({
1590
1555
  systems: switchSystems,
@@ -1616,7 +1581,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1616
1581
  { value: 'hijri', label: systemLabels.hijri },
1617
1582
  ]}
1618
1583
  selected={dualTab}
1619
- onSelect={setDualTab}
1584
+ onSelect={chooseTab}
1620
1585
  accessibilityLabel={systemA11yLabel}
1621
1586
  tokens={tokens}
1622
1587
  styles={styles}
@@ -1632,12 +1597,9 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1632
1597
  <View style={hijriShown ? RTL : undefined}>{node}</View>
1633
1598
  </RtlContext.Provider>
1634
1599
  );
1635
- // Another calendar opens on the days the last one showed, else on the
1636
- // selection (shown or not), else where the picker would open.
1637
- const calendarAnchor =
1638
- shownAnchor.current ??
1639
- (selectionShown ? null : firstSelectedDay(selection)) ??
1640
- 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;
1641
1603
 
1642
1604
  const calendar = (
1643
1605
  <View style={folded ? FOLDED : undefined}>
@@ -1646,10 +1608,10 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1646
1608
  {mirrored(
1647
1609
  <Calendar
1648
1610
  key={shownSystem}
1649
- constraints={selectionShown ? calendarConstraints : constraints}
1611
+ constraints={calendarConstraints}
1650
1612
  today={today}
1651
1613
  initialVisibleDate={calendarAnchor}
1652
- selection={selectionShown ? selection : emptySelection(selection)}
1614
+ selection={selection}
1653
1615
  onSelectDay={onSelectDayStable}
1654
1616
  firstDayOfWeek={firstDayOfWeek}
1655
1617
  showOutsideDays={showOutsideDays}
@@ -1725,14 +1687,11 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
1725
1687
  ? validation.message
1726
1688
  : null;
1727
1689
 
1728
- // The field writes the date in the digits of the calendar it was picked in;
1729
- // 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.
1730
1691
  const sheetContent = (
1731
1692
  <>
1732
- <NumeralsContext.Provider value={digitsFor(draftSystem)}>
1733
- {sheetField}
1734
- </NumeralsContext.Provider>
1735
1693
  <NumeralsContext.Provider value={digitsFor(shownSystem)}>
1694
+ {sheetField}
1736
1695
  {calendar}
1737
1696
  </NumeralsContext.Provider>
1738
1697
  {timeRow == null ? null : (
@@ -1,5 +1,5 @@
1
1
  import React, { memo } from 'react';
2
- import { Pressable, Text, View } from 'react-native';
2
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
3
3
  import { isSameDay } from '../../core/calendarDate';
4
4
  import type { CalendarDate } from '../../core/types';
5
5
  import type { PickerStyles } from '../theme/styleKeys';
@@ -35,9 +35,10 @@ interface Props extends DayCellState {
35
35
  }
36
36
 
37
37
  /**
38
- * One day: today ring, selected fill, range band, greyed (not pressable) when
39
- * disabled, holiday colour. Only primitive-ish props change between renders, so
40
- * memo keeps typing and taps from redrawing all 42 cells.
38
+ * One day: today ring (faded when today is disabled), selected fill,
39
+ * range band, greyed (not pressable) when disabled, holiday colour. Only
40
+ * primitive-ish props change between renders, so memo keeps typing and taps
41
+ * from redrawing all 42 cells.
41
42
  */
42
43
  const DayCell: React.FC<Props> = ({
43
44
  date,
@@ -72,6 +73,12 @@ const DayCell: React.FC<Props> = ({
72
73
  : rangeRole === 'end'
73
74
  ? s.fillEnd
74
75
  : null;
76
+ // The today ring. On a greyed today it's faded, in the day's shape.
77
+ const ring = isToday && !filled;
78
+ const fadedRing = ring && isDisabled;
79
+ const dayRadius = fadedRing
80
+ ? StyleSheet.flatten(styles?.day)?.borderRadius
81
+ : undefined;
75
82
 
76
83
  return (
77
84
  <View style={s.cellWrap}>
@@ -87,7 +94,7 @@ const DayCell: React.FC<Props> = ({
87
94
  style={({ pressed }) => [
88
95
  s.day,
89
96
  styles?.day,
90
- isToday && !filled && [s.today, styles?.today],
97
+ ring && !isDisabled && [s.today, styles?.today],
91
98
  filled && [s.selected, styles?.selected],
92
99
  rangeRole === 'start' && styles?.rangeStart,
93
100
  rangeRole === 'middle' && styles?.rangeMiddle,
@@ -98,6 +105,18 @@ const DayCell: React.FC<Props> = ({
98
105
  pressed && !isDisabled && !filled && [s.pressed, styles?.dayPressed],
99
106
  ]}
100
107
  >
108
+ {fadedRing ? (
109
+ <View
110
+ pointerEvents="none"
111
+ testID={testID ? `${testID}-today-ring` : undefined}
112
+ style={[
113
+ s.today,
114
+ styles?.today,
115
+ s.todayDisabledRing,
116
+ dayRadius !== undefined && { borderRadius: dayRadius },
117
+ ]}
118
+ />
119
+ ) : null}
101
120
  <Text
102
121
  maxFontSizeMultiplier={MAX_FONT_SCALE}
103
122
  style={[
@@ -107,7 +126,7 @@ const DayCell: React.FC<Props> = ({
107
126
  isHoliday &&
108
127
  showHolidays &&
109
128
  !filled && [s.holidayText, styles?.holidayText],
110
- isToday && !filled && styles?.todayText,
129
+ ring && styles?.todayText,
111
130
  isDisabled && [s.disabledText, styles?.disabledText],
112
131
  filled && [s.selectedText, styles?.selectedText],
113
132
  ]}
@@ -97,6 +97,17 @@ const create = (t: ThemeTokens) =>
97
97
  color: t.mutedText,
98
98
  },
99
99
  today: { borderWidth: 1, borderColor: t.accent },
100
+ // A greyed today: its accent ring, faded. A layer of its own, so the fade
101
+ // leaves the day number as grey as the other greyed days.
102
+ todayDisabledRing: {
103
+ position: 'absolute',
104
+ top: 0,
105
+ right: 0,
106
+ bottom: 0,
107
+ left: 0,
108
+ borderRadius: t.radius,
109
+ opacity: 0.4,
110
+ },
100
111
  selected: { backgroundColor: t.accent },
101
112
  selectedText: { color: t.onAccent, ...fontStyle(t, 'semiBold') },
102
113
  outsideText: { color: t.outsideText ?? t.mutedText },
@@ -21,6 +21,7 @@ import FieldMessages from './FieldMessages';
21
21
  import {
22
22
  formatValueParts,
23
23
  formatValueText,
24
+ emptyFieldParts,
24
25
  placeholderText,
25
26
  segmentErrorText,
26
27
  type DateTextFormatter,
@@ -240,6 +241,20 @@ const ValueField: React.FC<ValueFieldProps> = (props) => {
240
241
  styles?.fieldText,
241
242
  disabled && [s.disabledText, styles?.fieldTextDisabled],
242
243
  ];
244
+ // The empty field's placeholder sets its date apart from its time the same way.
245
+ const separator = strings.dateTimeSeparator ?? ' ';
246
+ const emptyParts =
247
+ styles?.fieldDateText && type === 'datetime'
248
+ ? emptyFieldParts(placeholder, type, dateFormat, timeFormat, strings)
249
+ : null;
250
+ const placeholderSplit = emptyParts?.time ? emptyParts : null;
251
+ const placeholderStyle = [
252
+ s.text,
253
+ styles?.fieldText,
254
+ s.placeholder,
255
+ styles?.fieldPlaceholder,
256
+ disabled && s.disabledText,
257
+ ];
243
258
 
244
259
  const clear = () => {
245
260
  dispatch({ type: 'reset', value: null });
@@ -335,7 +350,7 @@ const ValueField: React.FC<ValueFieldProps> = (props) => {
335
350
  ellipsizeMode="clip"
336
351
  style={[valueStyle, s.textScrolled, styles?.fieldDateText]}
337
352
  >
338
- {digits(`${parts.date}${strings.dateTimeSeparator ?? ' '}`)}
353
+ {digits(`${parts.date}${separator}`)}
339
354
  </Text>
340
355
  <Text numberOfLines={1} ellipsizeMode="clip" style={valueStyle}>
341
356
  {digits(parts.time)}
@@ -366,17 +381,21 @@ const ValueField: React.FC<ValueFieldProps> = (props) => {
366
381
  {digits(text)}
367
382
  </Text>
368
383
  </ScrollView>
384
+ ) : placeholderSplit ? (
385
+ <>
386
+ <Text
387
+ numberOfLines={1}
388
+ ellipsizeMode="clip"
389
+ style={[placeholderStyle, s.textScrolled, styles?.fieldDateText]}
390
+ >
391
+ {`${placeholderSplit.date}${separator}`}
392
+ </Text>
393
+ <Text numberOfLines={1} ellipsizeMode="clip" style={placeholderStyle}>
394
+ {placeholderSplit.time}
395
+ </Text>
396
+ </>
369
397
  ) : (
370
- <Text
371
- numberOfLines={1}
372
- style={[
373
- s.text,
374
- styles?.fieldText,
375
- s.placeholder,
376
- styles?.fieldPlaceholder,
377
- disabled && s.disabledText,
378
- ]}
379
- >
398
+ <Text numberOfLines={1} style={placeholderStyle}>
380
399
  {placeholder ||
381
400
  placeholderText(type, dateFormat, timeFormat, strings)}
382
401
  </Text>