rn-grid-date-time-picker 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/lib/commonjs/components/DateTimePicker.js +48 -20
- package/lib/commonjs/components/DateTimePicker.js.map +1 -1
- package/lib/commonjs/components/container/PickerContainer.js +2 -1
- package/lib/commonjs/components/container/PickerContainer.js.map +1 -1
- package/lib/commonjs/components/container/PickerLayout.js +5 -1
- package/lib/commonjs/components/container/PickerLayout.js.map +1 -1
- package/lib/commonjs/components/field/KeyboardAccessoryBar.js +38 -0
- package/lib/commonjs/components/field/KeyboardAccessoryBar.js.map +1 -0
- package/lib/commonjs/components/field/SegmentedInput.js +11 -6
- package/lib/commonjs/components/field/SegmentedInput.js.map +1 -1
- package/lib/commonjs/components/hooks/useScrollToTime.js +85 -0
- package/lib/commonjs/components/hooks/useScrollToTime.js.map +1 -0
- package/lib/commonjs/components/theme/defaultStrings.js.map +1 -1
- package/lib/commonjs/components/time/TypedTime.js +34 -10
- package/lib/commonjs/components/time/TypedTime.js.map +1 -1
- package/lib/commonjs/components/types.js +2 -0
- package/lib/commonjs/components/types.js.map +1 -1
- package/lib/commonjs/core/draft.js +2 -1
- package/lib/commonjs/core/draft.js.map +1 -1
- package/lib/module/components/DateTimePicker.js +48 -20
- package/lib/module/components/DateTimePicker.js.map +1 -1
- package/lib/module/components/container/PickerContainer.js +2 -1
- package/lib/module/components/container/PickerContainer.js.map +1 -1
- package/lib/module/components/container/PickerLayout.js +5 -1
- package/lib/module/components/container/PickerLayout.js.map +1 -1
- package/lib/module/components/field/KeyboardAccessoryBar.js +33 -0
- package/lib/module/components/field/KeyboardAccessoryBar.js.map +1 -0
- package/lib/module/components/field/SegmentedInput.js +11 -7
- package/lib/module/components/field/SegmentedInput.js.map +1 -1
- package/lib/module/components/hooks/useScrollToTime.js +79 -0
- package/lib/module/components/hooks/useScrollToTime.js.map +1 -0
- package/lib/module/components/theme/defaultStrings.js.map +1 -1
- package/lib/module/components/time/TypedTime.js +33 -11
- package/lib/module/components/time/TypedTime.js.map +1 -1
- package/lib/module/components/types.js +2 -0
- package/lib/module/components/types.js.map +1 -1
- package/lib/module/core/draft.js +2 -1
- package/lib/module/core/draft.js.map +1 -1
- package/lib/typescript/commonjs/src/components/DateTimePicker.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/components/container/PickerContainer.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/components/container/PickerLayout.d.ts +7 -2
- package/lib/typescript/commonjs/src/components/container/PickerLayout.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/components/field/KeyboardAccessoryBar.d.ts +15 -0
- package/lib/typescript/commonjs/src/components/field/KeyboardAccessoryBar.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/components/field/SegmentedInput.d.ts +2 -2
- package/lib/typescript/commonjs/src/components/field/SegmentedInput.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/components/hooks/useScrollToTime.d.ts +26 -0
- package/lib/typescript/commonjs/src/components/hooks/useScrollToTime.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/components/nativeTypes.d.ts +2 -1
- package/lib/typescript/commonjs/src/components/nativeTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/components/theme/defaultStrings.d.ts +1 -0
- package/lib/typescript/commonjs/src/components/theme/defaultStrings.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/components/time/TypedTime.d.ts +1 -1
- package/lib/typescript/commonjs/src/components/time/TypedTime.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/components/types.d.ts +41 -3
- package/lib/typescript/commonjs/src/components/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/core/draft.d.ts +2 -1
- package/lib/typescript/commonjs/src/core/draft.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +1 -1
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/components/DateTimePicker.d.ts.map +1 -1
- package/lib/typescript/module/src/components/container/PickerContainer.d.ts.map +1 -1
- package/lib/typescript/module/src/components/container/PickerLayout.d.ts +7 -2
- package/lib/typescript/module/src/components/container/PickerLayout.d.ts.map +1 -1
- package/lib/typescript/module/src/components/field/KeyboardAccessoryBar.d.ts +15 -0
- package/lib/typescript/module/src/components/field/KeyboardAccessoryBar.d.ts.map +1 -0
- package/lib/typescript/module/src/components/field/SegmentedInput.d.ts +2 -2
- package/lib/typescript/module/src/components/field/SegmentedInput.d.ts.map +1 -1
- package/lib/typescript/module/src/components/hooks/useScrollToTime.d.ts +26 -0
- package/lib/typescript/module/src/components/hooks/useScrollToTime.d.ts.map +1 -0
- package/lib/typescript/module/src/components/nativeTypes.d.ts +2 -1
- package/lib/typescript/module/src/components/nativeTypes.d.ts.map +1 -1
- package/lib/typescript/module/src/components/theme/defaultStrings.d.ts +1 -0
- package/lib/typescript/module/src/components/theme/defaultStrings.d.ts.map +1 -1
- package/lib/typescript/module/src/components/time/TypedTime.d.ts +1 -1
- package/lib/typescript/module/src/components/time/TypedTime.d.ts.map +1 -1
- package/lib/typescript/module/src/components/types.d.ts +41 -3
- package/lib/typescript/module/src/components/types.d.ts.map +1 -1
- package/lib/typescript/module/src/core/draft.d.ts +2 -1
- package/lib/typescript/module/src/core/draft.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +1 -1
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/DateTimePicker.tsx +67 -20
- package/src/components/container/PickerContainer.tsx +1 -0
- package/src/components/container/PickerLayout.tsx +10 -1
- package/src/components/field/KeyboardAccessoryBar.tsx +38 -0
- package/src/components/field/SegmentedInput.tsx +12 -7
- package/src/components/hooks/useScrollToTime.ts +99 -0
- package/src/components/nativeTypes.ts +2 -1
- package/src/components/theme/defaultStrings.ts +1 -0
- package/src/components/time/TypedTime.tsx +31 -10
- package/src/components/types.ts +45 -3
- package/src/core/draft.ts +5 -2
- package/src/index.tsx +1 -0
|
@@ -70,6 +70,7 @@ import RangeFields, { type RangeEnd } from './field/RangeFields';
|
|
|
70
70
|
import ValueField from './field/ValueField';
|
|
71
71
|
import { useKeyboardFold } from './hooks/useKeyboardFold';
|
|
72
72
|
import { useOpenPicker } from './hooks/useOpenPicker';
|
|
73
|
+
import { useScrollToTime } from './hooks/useScrollToTime';
|
|
73
74
|
import { usePickerDraft } from './hooks/usePickerDraft';
|
|
74
75
|
import { useTodayTick } from './hooks/useTodayTick';
|
|
75
76
|
import TimeRows, { type PendingTimes } from './time/TimeRows';
|
|
@@ -138,6 +139,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
138
139
|
multiple,
|
|
139
140
|
range,
|
|
140
141
|
presentation = 'sheet',
|
|
142
|
+
scrollToTimeRef,
|
|
141
143
|
visible,
|
|
142
144
|
onOpen,
|
|
143
145
|
onClose,
|
|
@@ -188,6 +190,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
188
190
|
calendarSwitch,
|
|
189
191
|
onCalendarSystemChange,
|
|
190
192
|
dualDefaultTab = 'gregory',
|
|
193
|
+
fieldCalendar = 'picked',
|
|
191
194
|
hijriLocale = 'ar-SA',
|
|
192
195
|
renderCalendarSwitch,
|
|
193
196
|
showToday = false,
|
|
@@ -432,12 +435,13 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
432
435
|
|
|
433
436
|
const rangeLayout = range?.layout ?? 'singleField';
|
|
434
437
|
const multipleTimeMode = multiple?.timeMode ?? 'shared';
|
|
438
|
+
const startsEmpty = newDayTime === 'empty';
|
|
435
439
|
const draftOptions = useMemo((): DraftOptions => {
|
|
436
440
|
return {
|
|
437
441
|
type,
|
|
438
442
|
zone,
|
|
439
443
|
adapter,
|
|
440
|
-
defaultTime: DEFAULT_TIME,
|
|
444
|
+
defaultTime: startsEmpty ? null : DEFAULT_TIME,
|
|
441
445
|
multiple: {
|
|
442
446
|
timeMode: multiple?.timeMode,
|
|
443
447
|
maxSelections: multiple?.maxSelections,
|
|
@@ -453,6 +457,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
453
457
|
type,
|
|
454
458
|
zone,
|
|
455
459
|
adapter,
|
|
460
|
+
startsEmpty,
|
|
456
461
|
multiple?.timeMode,
|
|
457
462
|
multiple?.maxSelections,
|
|
458
463
|
range?.allowSameDay,
|
|
@@ -672,6 +677,28 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
672
677
|
const previewIssue = validateDraft(preview, draftOptions);
|
|
673
678
|
|
|
674
679
|
const d = draft.draft;
|
|
680
|
+
// Once a calendar tap picks the date (both ends of a range), the sheet (or
|
|
681
|
+
// the app's ScrollView around an inline picker) scrolls the time section
|
|
682
|
+
// into view.
|
|
683
|
+
const datePicked =
|
|
684
|
+
d.mode === 'single'
|
|
685
|
+
? !!d.slot.value
|
|
686
|
+
: d.mode === 'range'
|
|
687
|
+
? !!d.from.value && !!d.to.value
|
|
688
|
+
: d.slots.length > 0;
|
|
689
|
+
const {
|
|
690
|
+
scrollRef,
|
|
691
|
+
timeRef,
|
|
692
|
+
request: revealTime,
|
|
693
|
+
} = useScrollToTime(
|
|
694
|
+
type !== 'datetime'
|
|
695
|
+
? false
|
|
696
|
+
: isInline
|
|
697
|
+
? !!scrollToTimeRef
|
|
698
|
+
: (sheetOptions?.scrollToTime ?? true),
|
|
699
|
+
datePicked,
|
|
700
|
+
isInline ? scrollToTimeRef : undefined
|
|
701
|
+
);
|
|
675
702
|
/** The values a draft would save, with a shared time applied. */
|
|
676
703
|
const valuesOf = (dr: Draft): (DateTimeValue | null)[] => {
|
|
677
704
|
if (dr.mode === 'single') return [dr.slot.value];
|
|
@@ -928,7 +955,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
928
955
|
}, [closePicker]);
|
|
929
956
|
|
|
930
957
|
// 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.
|
|
958
|
+
// newDayTime, else a time already typed in the time row, else now (or none).
|
|
932
959
|
const withFilledTime = (
|
|
933
960
|
parsed: ParsedSegments,
|
|
934
961
|
key: 'single' | RangeEnd
|
|
@@ -937,7 +964,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
937
964
|
(parsed.dateOnly
|
|
938
965
|
? {
|
|
939
966
|
date: parsed.dateOnly,
|
|
940
|
-
time: fixedTime ?? pending[key] ??
|
|
967
|
+
time: fixedTime ?? pending[key] ?? newTime(),
|
|
941
968
|
}
|
|
942
969
|
: null);
|
|
943
970
|
|
|
@@ -967,7 +994,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
967
994
|
};
|
|
968
995
|
|
|
969
996
|
// A fixed time for every newly picked day (wins over a typed time and over "now").
|
|
970
|
-
const fixedTime = newDayTime
|
|
997
|
+
const fixedTime = typeof newDayTime === 'object' ? newDayTime : null;
|
|
971
998
|
|
|
972
999
|
// A day picked on an empty date-time starts at the current time, moved up to
|
|
973
1000
|
// the next allowed minute step. An existing time is kept.
|
|
@@ -978,6 +1005,11 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
978
1005
|
const capped = Math.min(minutes, 23 * 60 + 60 - step);
|
|
979
1006
|
return { hour: Math.floor(capped / 60), minute: capped % 60 };
|
|
980
1007
|
}, [now, zone, minuteStep]);
|
|
1008
|
+
/** A new day's time when nothing fixed or typed: now, or none with newDayTime 'empty'. */
|
|
1009
|
+
const newTime = useCallback(
|
|
1010
|
+
(): Time | null => (startsEmpty ? null : nowTime()),
|
|
1011
|
+
[startsEmpty, nowTime]
|
|
1012
|
+
);
|
|
981
1013
|
|
|
982
1014
|
// A range end picked on the start's day starts an hour after the start (at
|
|
983
1015
|
// most the day's latest allowed time, or its last step), so the range isn't
|
|
@@ -987,10 +1019,10 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
987
1019
|
from: DateTimeValue | null,
|
|
988
1020
|
day: CalendarDate,
|
|
989
1021
|
typed: Time | null | undefined
|
|
990
|
-
): Time => {
|
|
1022
|
+
): Time | null => {
|
|
991
1023
|
if (!fixedTime && typed) return typed;
|
|
992
|
-
const time = fixedTime ??
|
|
993
|
-
if (!from?.time || !isSameDay(from.date, day)) return time;
|
|
1024
|
+
const time = fixedTime ?? newTime();
|
|
1025
|
+
if (!time || !from?.time || !isSameDay(from.date, day)) return time;
|
|
994
1026
|
const start = from.time.hour * 60 + from.time.minute;
|
|
995
1027
|
if (time.hour * 60 + time.minute > start) return time;
|
|
996
1028
|
// Within the day's allowed hours when it has them.
|
|
@@ -1018,6 +1050,8 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
1018
1050
|
draft.clear();
|
|
1019
1051
|
resetSelectionUi();
|
|
1020
1052
|
}
|
|
1053
|
+
// A multiple picker scrolls for its first date only, so more can be picked.
|
|
1054
|
+
if (cur.mode !== 'multiple' || cur.slots.length === 0) revealTime();
|
|
1021
1055
|
if (cur.mode === 'range') {
|
|
1022
1056
|
if (rangeLayout === 'fromTo') {
|
|
1023
1057
|
setEndPicked(false);
|
|
@@ -1032,7 +1066,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
1032
1066
|
const end = moveNearest ? nearest : 'from';
|
|
1033
1067
|
setTypingBlock('from', false);
|
|
1034
1068
|
setTypingBlock('to', false);
|
|
1035
|
-
draft.selectDay(day, fixedTime ?? times[end] ??
|
|
1069
|
+
draft.selectDay(day, fixedTime ?? times[end] ?? newTime());
|
|
1036
1070
|
setActiveEnd(moveNearest ? nearest : 'to');
|
|
1037
1071
|
return;
|
|
1038
1072
|
}
|
|
@@ -1051,7 +1085,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
1051
1085
|
end1,
|
|
1052
1086
|
end1 === 'to'
|
|
1053
1087
|
? endTimeFor(cur.from.value, day, times.to)
|
|
1054
|
-
: (fixedTime ?? times.from ??
|
|
1088
|
+
: (fixedTime ?? times.from ?? newTime())
|
|
1055
1089
|
);
|
|
1056
1090
|
// The start tapped again where a range can't be one day deselects
|
|
1057
1091
|
// it, so the next tap picks the start again.
|
|
@@ -1073,7 +1107,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
1073
1107
|
day,
|
|
1074
1108
|
end === 'to' && !cur.to.value
|
|
1075
1109
|
? endTimeFor(cur.from.value, day, times.to)
|
|
1076
|
-
: (fixedTime ?? times[end] ??
|
|
1110
|
+
: (fixedTime ?? times[end] ?? newTime())
|
|
1077
1111
|
);
|
|
1078
1112
|
return;
|
|
1079
1113
|
}
|
|
@@ -1087,18 +1121,18 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
1087
1121
|
setActiveDay(day);
|
|
1088
1122
|
return;
|
|
1089
1123
|
}
|
|
1090
|
-
draft.selectDay(day, fixedTime ??
|
|
1124
|
+
draft.selectDay(day, fixedTime ?? newTime());
|
|
1091
1125
|
if (perDate) setActiveDay(picked ? null : day);
|
|
1092
1126
|
return;
|
|
1093
1127
|
}
|
|
1094
1128
|
setTypingBlock('single', false);
|
|
1095
|
-
draft.selectDay(day, fixedTime ?? times.single ??
|
|
1129
|
+
draft.selectDay(day, fixedTime ?? times.single ?? newTime());
|
|
1096
1130
|
},
|
|
1097
1131
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- valuesOf reads only `d` and props listed here
|
|
1098
1132
|
[
|
|
1099
1133
|
d,
|
|
1100
1134
|
draft,
|
|
1101
|
-
|
|
1135
|
+
newTime,
|
|
1102
1136
|
rangeLayout,
|
|
1103
1137
|
activeEnd,
|
|
1104
1138
|
activeDay,
|
|
@@ -1115,6 +1149,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
1115
1149
|
draftOptions,
|
|
1116
1150
|
minuteStep,
|
|
1117
1151
|
constraints,
|
|
1152
|
+
revealTime,
|
|
1118
1153
|
]
|
|
1119
1154
|
);
|
|
1120
1155
|
|
|
@@ -1271,9 +1306,11 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
1271
1306
|
? (v) =>
|
|
1272
1307
|
hijriDateText(v.date, hijriLocale) ?? formatDateText(v, dateFormat)
|
|
1273
1308
|
: undefined;
|
|
1274
|
-
|
|
1309
|
+
// The calendar the on-screen field writes the saved value in.
|
|
1310
|
+
const fieldSystem = fieldCalendar === 'gregory' ? 'gregory' : savedSystem;
|
|
1311
|
+
const savedDateText = dateTextFor(fieldSystem);
|
|
1275
1312
|
const draftDateText = dateTextFor(draftSystem);
|
|
1276
|
-
const savedStrings = stringsFor(
|
|
1313
|
+
const savedStrings = stringsFor(fieldSystem);
|
|
1277
1314
|
const draftStrings = stringsFor(draftSystem);
|
|
1278
1315
|
// Error messages inside the picker follow the tab on screen; the on-screen
|
|
1279
1316
|
// field's keep `strings`.
|
|
@@ -1353,7 +1390,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
1353
1390
|
type,
|
|
1354
1391
|
mode,
|
|
1355
1392
|
value,
|
|
1356
|
-
displayText: digitsFor(
|
|
1393
|
+
displayText: digitsFor(fieldSystem)(
|
|
1357
1394
|
savedText ??
|
|
1358
1395
|
formatValueText(
|
|
1359
1396
|
savedSingle,
|
|
@@ -1698,9 +1735,18 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
1698
1735
|
<NumeralsContext.Provider value={digitsFor(shownSystem)}>
|
|
1699
1736
|
{calendar}
|
|
1700
1737
|
</NumeralsContext.Provider>
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1738
|
+
{timeRow == null ? null : (
|
|
1739
|
+
// Measured for the scroll to the time (sheet.scrollToTime).
|
|
1740
|
+
<View
|
|
1741
|
+
ref={timeRef}
|
|
1742
|
+
collapsable={false}
|
|
1743
|
+
testID={testID ? `${testID}-time-section` : undefined}
|
|
1744
|
+
>
|
|
1745
|
+
<NumeralsContext.Provider value={digitsFor(shownSystem)}>
|
|
1746
|
+
{mirrored(timeRow)}
|
|
1747
|
+
</NumeralsContext.Provider>
|
|
1748
|
+
</View>
|
|
1749
|
+
)}
|
|
1704
1750
|
{validationMessage ? (
|
|
1705
1751
|
<FieldMessages
|
|
1706
1752
|
error={validationMessage}
|
|
@@ -1716,7 +1762,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
1716
1762
|
);
|
|
1717
1763
|
|
|
1718
1764
|
return (
|
|
1719
|
-
<NumeralsContext.Provider value={digitsFor(
|
|
1765
|
+
<NumeralsContext.Provider value={digitsFor(fieldSystem)}>
|
|
1720
1766
|
{trigger}
|
|
1721
1767
|
<PickerContainer
|
|
1722
1768
|
presentation={presentation}
|
|
@@ -1724,6 +1770,7 @@ const DateTimePicker: React.FC<DateTimePickerProps> = (props) => {
|
|
|
1724
1770
|
onRequestClose={close}
|
|
1725
1771
|
renderSheet={renderSheet}
|
|
1726
1772
|
sheet={sheetOptions}
|
|
1773
|
+
scrollRef={scrollRef}
|
|
1727
1774
|
state={state}
|
|
1728
1775
|
header={header}
|
|
1729
1776
|
title={title ?? (isInline ? undefined : strings.titleDate)}
|
|
@@ -36,6 +36,7 @@ const PickerContainer: React.FC<PickerContainerProps> = ({
|
|
|
36
36
|
fixedHeader: sheet?.fixedHeader,
|
|
37
37
|
fixedFooter: sheet?.fixedFooter,
|
|
38
38
|
fill: !renderSheet && sheet?.height !== undefined,
|
|
39
|
+
scrollProps: sheet?.scrollProps,
|
|
39
40
|
});
|
|
40
41
|
|
|
41
42
|
if (presentation === 'inline') {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Pressable, ScrollView, Text, View } from 'react-native';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ScrollViewRef } from '../nativeTypes';
|
|
4
|
+
import type { PickerState, SheetScrollProps, Slot } from '../types';
|
|
4
5
|
import type { PickerStyles } from '../theme/styleKeys';
|
|
5
6
|
import { buildContainerStyles } from './containerStyles';
|
|
6
7
|
|
|
@@ -27,6 +28,10 @@ export interface PickerLayoutProps {
|
|
|
27
28
|
fixedFooter?: boolean;
|
|
28
29
|
/** The sheet has a fixed height: the body fills it. */
|
|
29
30
|
fill?: boolean;
|
|
31
|
+
/** Sheet only: passed to the body's ScrollView over its defaults. */
|
|
32
|
+
scrollProps?: SheetScrollProps;
|
|
33
|
+
/** Sheet only: the body's ScrollView. */
|
|
34
|
+
scrollRef?: React.Ref<ScrollViewRef>;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
const renderSlot = (
|
|
@@ -58,6 +63,8 @@ export const buildPickerLayout = ({
|
|
|
58
63
|
fixedHeader = true,
|
|
59
64
|
fixedFooter = true,
|
|
60
65
|
fill,
|
|
66
|
+
scrollProps,
|
|
67
|
+
scrollRef,
|
|
61
68
|
}: PickerLayoutProps): { body: React.ReactNode; footer: React.ReactNode } => {
|
|
62
69
|
const s = buildContainerStyles(state.tokens);
|
|
63
70
|
|
|
@@ -129,12 +136,14 @@ export const buildPickerLayout = ({
|
|
|
129
136
|
{headerScrolls ? null : headerNode}
|
|
130
137
|
{scrollBody ? (
|
|
131
138
|
<ScrollView
|
|
139
|
+
ref={scrollRef}
|
|
132
140
|
testID={testID ? `${testID}-scroll` : undefined}
|
|
133
141
|
style={s.scroll}
|
|
134
142
|
keyboardShouldPersistTaps="handled"
|
|
135
143
|
nestedScrollEnabled
|
|
136
144
|
bounces={false}
|
|
137
145
|
showsVerticalScrollIndicator={false}
|
|
146
|
+
{...scrollProps}
|
|
138
147
|
>
|
|
139
148
|
{headerScrolls ? headerNode : null}
|
|
140
149
|
{content}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { InputAccessoryView, Platform, StyleSheet, View } from 'react-native';
|
|
3
|
+
import type { ThemeTokens } from '../theme/tokens';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** The `inputAccessoryViewID` of the one box it serves. */
|
|
7
|
+
nativeID: string;
|
|
8
|
+
tokens: ThemeTokens;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* iOS only: an empty solid bar above a box's number pad. Without it React
|
|
13
|
+
* Native adds its own Done bar, which is see-through on newer iOS, so a sheet
|
|
14
|
+
* resting on the keyboard showed the screen behind it.
|
|
15
|
+
*/
|
|
16
|
+
const KeyboardAccessoryBar: React.FC<Props> = ({ nativeID, tokens }) => {
|
|
17
|
+
if (Platform.OS !== 'ios') return null;
|
|
18
|
+
return (
|
|
19
|
+
<InputAccessoryView nativeID={nativeID}>
|
|
20
|
+
<View
|
|
21
|
+
style={[
|
|
22
|
+
styles.bar,
|
|
23
|
+
{
|
|
24
|
+
height: tokens.touchTarget,
|
|
25
|
+
backgroundColor: tokens.surface,
|
|
26
|
+
borderTopColor: tokens.border,
|
|
27
|
+
},
|
|
28
|
+
]}
|
|
29
|
+
/>
|
|
30
|
+
</InputAccessoryView>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const styles = StyleSheet.create({
|
|
35
|
+
bar: { borderTopWidth: StyleSheet.hairlineWidth },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export default KeyboardAccessoryBar;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useRef } from 'react';
|
|
1
|
+
import React, { useEffect, useId, useMemo, useRef } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Pressable,
|
|
4
4
|
StyleSheet,
|
|
@@ -20,6 +20,7 @@ import { MAX_FONT_SCALE } from '../theme/a11y';
|
|
|
20
20
|
import { useNumerals } from '../theme/numerals';
|
|
21
21
|
import type { ThemeTokens } from '../theme/tokens';
|
|
22
22
|
import { buildFieldStyles } from './fieldStyles';
|
|
23
|
+
import KeyboardAccessoryBar from './KeyboardAccessoryBar';
|
|
23
24
|
|
|
24
25
|
interface Props {
|
|
25
26
|
state: SegmentEditorState;
|
|
@@ -93,8 +94,8 @@ export const changeToActions = (
|
|
|
93
94
|
|
|
94
95
|
/**
|
|
95
96
|
* The typed field: one box per segment in locale order, plus an AM/PM switch for
|
|
96
|
-
* 12-hour date-times. Number pad, with
|
|
97
|
-
*
|
|
97
|
+
* 12-hour date-times. Number pad, with an empty solid bar above it on iOS (so
|
|
98
|
+
* React Native adds no see-through Done bar).
|
|
98
99
|
*/
|
|
99
100
|
const SegmentedInput: React.FC<Props> = ({
|
|
100
101
|
state,
|
|
@@ -108,6 +109,7 @@ const SegmentedInput: React.FC<Props> = ({
|
|
|
108
109
|
testID,
|
|
109
110
|
}) => {
|
|
110
111
|
const digits = useNumerals();
|
|
112
|
+
const barId = useId();
|
|
111
113
|
// Each box's text as last rendered, and as it last reported. Fast input (a
|
|
112
114
|
// hardware keyboard, autofill) sends the next character before React has
|
|
113
115
|
// re-rendered, and the native box may ignore React's reset while it's
|
|
@@ -197,10 +199,9 @@ const SegmentedInput: React.FC<Props> = ({
|
|
|
197
199
|
keyboardType="number-pad"
|
|
198
200
|
editable={!disabled}
|
|
199
201
|
contextMenuHidden
|
|
200
|
-
// iOS
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
inputAccessoryViewButtonLabel={strings.done}
|
|
202
|
+
// iOS: this box's own solid bar (below), so React Native adds no
|
|
203
|
+
// see-through Done bar.
|
|
204
|
+
inputAccessoryViewID={`${barId}-${spec.key}`}
|
|
204
205
|
onFocus={() => dispatch({ type: 'focus', key: spec.key })}
|
|
205
206
|
onBlur={() => {
|
|
206
207
|
if ((getState?.() ?? state).focused === spec.key)
|
|
@@ -228,6 +229,10 @@ const SegmentedInput: React.FC<Props> = ({
|
|
|
228
229
|
isFocused && [s.segmentFocused, styles?.fieldSegmentFocused],
|
|
229
230
|
]}
|
|
230
231
|
/>
|
|
232
|
+
<KeyboardAccessoryBar
|
|
233
|
+
nativeID={`${barId}-${spec.key}`}
|
|
234
|
+
tokens={tokens}
|
|
235
|
+
/>
|
|
231
236
|
</React.Fragment>
|
|
232
237
|
);
|
|
233
238
|
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type RefObject,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import type { ScrollViewRef, ViewRef } from '../nativeTypes';
|
|
9
|
+
import { useReduceMotion } from './useReduceMotion';
|
|
10
|
+
|
|
11
|
+
export interface ScrollToTime {
|
|
12
|
+
/** The sheet body's ScrollView. */
|
|
13
|
+
scrollRef: RefObject<ScrollViewRef | null>;
|
|
14
|
+
/** The time section. */
|
|
15
|
+
timeRef: RefObject<ViewRef | null>;
|
|
16
|
+
/** Called from the calendar tap that picks the date. */
|
|
17
|
+
request: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Rounding in the measured positions, in points. */
|
|
21
|
+
const SLACK = 1;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Scrolls `scroll` so `section` shows: just far enough to show all of it (its
|
|
25
|
+
* top, when it's taller than the view), or to the end of the content with
|
|
26
|
+
* `toEnd`. Nothing moves while it already shows in full.
|
|
27
|
+
*/
|
|
28
|
+
export const scrollSectionIntoView = (
|
|
29
|
+
scroll: ScrollViewRef,
|
|
30
|
+
section: ViewRef,
|
|
31
|
+
toEnd: boolean,
|
|
32
|
+
animated: boolean
|
|
33
|
+
): void => {
|
|
34
|
+
scroll.measureInWindow((_vx, viewTop, _vw, viewHeight) => {
|
|
35
|
+
section.measureInWindow((_x, top, _w, height) => {
|
|
36
|
+
// Not laid out, or folded away.
|
|
37
|
+
if (!viewHeight || !height) return;
|
|
38
|
+
const fromTop = top - viewTop;
|
|
39
|
+
if (fromTop >= -SLACK && fromTop + height <= viewHeight + SLACK) return;
|
|
40
|
+
if (toEnd) {
|
|
41
|
+
scroll.scrollToEnd({ animated });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Where the section sits in the scrolled content.
|
|
45
|
+
const content = scroll.getInnerViewRef?.();
|
|
46
|
+
if (!content) return;
|
|
47
|
+
section.measureLayout(
|
|
48
|
+
content,
|
|
49
|
+
(_cx, y) => {
|
|
50
|
+
const target =
|
|
51
|
+
fromTop < 0 || height > viewHeight ? y : y + height - viewHeight;
|
|
52
|
+
scroll.scrollTo({ y: Math.max(0, target), animated });
|
|
53
|
+
},
|
|
54
|
+
() => {}
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The sheet's scroll to the time section once a calendar tap picks the date.
|
|
62
|
+
* `request()` from the tap; the scroll runs after that render, when `ready`
|
|
63
|
+
* (the picked date is complete) holds for it. Refs go on the body's ScrollView
|
|
64
|
+
* and the time section; `outerRef` (an inline picker's own ScrollView, from
|
|
65
|
+
* the app) is scrolled instead when given. 'end' scrolls to the end of the
|
|
66
|
+
* body; false never scrolls.
|
|
67
|
+
*/
|
|
68
|
+
export const useScrollToTime = (
|
|
69
|
+
mode: boolean | 'end',
|
|
70
|
+
ready: boolean,
|
|
71
|
+
outerRef?: RefObject<ScrollViewRef | null>
|
|
72
|
+
): ScrollToTime => {
|
|
73
|
+
const scrollRef = useRef<ScrollViewRef>(null);
|
|
74
|
+
const timeRef = useRef<ViewRef>(null);
|
|
75
|
+
const reduceMotion = useReduceMotion();
|
|
76
|
+
const [tapped, setTapped] = useState(0);
|
|
77
|
+
const on = useRef(mode !== false);
|
|
78
|
+
on.current = mode !== false;
|
|
79
|
+
const request = useCallback(() => {
|
|
80
|
+
if (on.current) setTapped((n) => n + 1);
|
|
81
|
+
}, []);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!tapped || !ready || mode === false) return;
|
|
84
|
+
// A frame after the tap, on iOS and Android alike: the old architecture
|
|
85
|
+
// lays the tap's changes out after this effect, so measuring now could
|
|
86
|
+
// read the layout before them; and Android runs a scroll sent in the frame
|
|
87
|
+
// that grows the content (a per-date row showing) before it grows, so the
|
|
88
|
+
// scroll would stop short.
|
|
89
|
+
const frame = requestAnimationFrame(() => {
|
|
90
|
+
const scroll = outerRef?.current ?? scrollRef.current;
|
|
91
|
+
const section = timeRef.current;
|
|
92
|
+
if (scroll && section)
|
|
93
|
+
scrollSectionIntoView(scroll, section, mode === 'end', !reduceMotion);
|
|
94
|
+
});
|
|
95
|
+
return () => cancelAnimationFrame(frame);
|
|
96
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- once per tap
|
|
97
|
+
}, [tapped]);
|
|
98
|
+
return { scrollRef, timeRef, request };
|
|
99
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentRef } from 'react';
|
|
2
|
-
import type { ScrollView, TextInput, TextInputProps } from 'react-native';
|
|
2
|
+
import type { ScrollView, TextInput, TextInputProps, View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* React Native types spelled so every supported version has them: 0.88 added
|
|
@@ -8,6 +8,7 @@ import type { ScrollView, TextInput, TextInputProps } from 'react-native';
|
|
|
8
8
|
*/
|
|
9
9
|
export type TextInputRef = ComponentRef<typeof TextInput>;
|
|
10
10
|
export type ScrollViewRef = ComponentRef<typeof ScrollView>;
|
|
11
|
+
export type ViewRef = ComponentRef<typeof View>;
|
|
11
12
|
export type KeyPressEvent = Parameters<
|
|
12
13
|
NonNullable<TextInputProps['onKeyPress']>
|
|
13
14
|
>[0];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Pressable, Text, TextInput, View } from 'react-native';
|
|
1
|
+
import React, { useId, useRef } from 'react';
|
|
2
|
+
import { Platform, Pressable, Text, TextInput, View } from 'react-native';
|
|
3
|
+
import KeyboardAccessoryBar from '../field/KeyboardAccessoryBar';
|
|
3
4
|
import type { TextInputRef } from '../nativeTypes';
|
|
4
5
|
import type { TimeBounds } from '../../core/constraints';
|
|
5
6
|
import type { Period } from '../../core/segments';
|
|
@@ -39,7 +40,7 @@ const PERIODS: Period[] = ['AM', 'PM'];
|
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* `[hh] : [mm] [AM | PM]`, or `[HH] : [mm]` in 24-hour. The boxes are plain
|
|
42
|
-
* TextInputs; the row (TimeRow) owns their values. Number pad, with a
|
|
43
|
+
* TextInputs; the row (TimeRow) owns their values. Number pad, with a solid bar
|
|
43
44
|
* on iOS. The AM/PM side that can't hold an allowed time is disabled.
|
|
44
45
|
*/
|
|
45
46
|
const TypedTime: React.FC<Props> = ({
|
|
@@ -61,10 +62,27 @@ const TypedTime: React.FC<Props> = ({
|
|
|
61
62
|
testID,
|
|
62
63
|
}) => {
|
|
63
64
|
const digits = useNumerals();
|
|
65
|
+
const barId = useId();
|
|
64
66
|
const s = buildTimeStyles(tokens);
|
|
67
|
+
const inputs = useRef<Partial<Record<TimeKey, TextInputRef | null>>>({});
|
|
68
|
+
|
|
69
|
+
// Android: a TextInput keeps any drag that starts on it, so the sheet can't
|
|
70
|
+
// scroll from a box. The boxes ignore touches and their wrapper takes them:
|
|
71
|
+
// a drag scrolls the sheet, a tap focuses the box (and brings the keyboard
|
|
72
|
+
// back). None of this changes with focus: flipping a box's touch handling
|
|
73
|
+
// as it gained focus made Android pass focus on to the next box, round and
|
|
74
|
+
// round. The wrapper is never focusable itself.
|
|
75
|
+
const passTouches = Platform.OS === 'android' && !disabled;
|
|
65
76
|
|
|
66
77
|
const box = (key: TimeKey) => (
|
|
67
|
-
<
|
|
78
|
+
<Pressable
|
|
79
|
+
style={s.boxWrap}
|
|
80
|
+
accessible={false}
|
|
81
|
+
focusable={false}
|
|
82
|
+
disabled={!passTouches}
|
|
83
|
+
onPress={() => inputs.current[key]?.focus()}
|
|
84
|
+
testID={testID ? `${testID}-${key}-wrap` : undefined}
|
|
85
|
+
>
|
|
68
86
|
<View
|
|
69
87
|
style={[
|
|
70
88
|
s.boxContainer,
|
|
@@ -73,9 +91,12 @@ const TypedTime: React.FC<Props> = ({
|
|
|
73
91
|
hasError && [s.boxError, styles?.timeBoxError],
|
|
74
92
|
]}
|
|
75
93
|
>
|
|
76
|
-
<View style={s.boxInner}>
|
|
94
|
+
<View style={s.boxInner} pointerEvents={passTouches ? 'none' : 'auto'}>
|
|
77
95
|
<TextInput
|
|
78
|
-
ref={(r) =>
|
|
96
|
+
ref={(r) => {
|
|
97
|
+
inputs.current[key] = r;
|
|
98
|
+
setRef(key, r);
|
|
99
|
+
}}
|
|
79
100
|
testID={testID ? `${testID}-${key}` : undefined}
|
|
80
101
|
accessibilityLabel={
|
|
81
102
|
key === 'hour' ? strings.a11yHour : strings.a11yMinute
|
|
@@ -99,9 +120,8 @@ const TypedTime: React.FC<Props> = ({
|
|
|
99
120
|
contextMenuHidden
|
|
100
121
|
editable={!disabled}
|
|
101
122
|
underlineColorAndroid="transparent"
|
|
102
|
-
// iOS
|
|
103
|
-
|
|
104
|
-
inputAccessoryViewButtonLabel={strings.done}
|
|
123
|
+
// iOS: this box's own solid bar (below).
|
|
124
|
+
inputAccessoryViewID={`${barId}-${key}`}
|
|
105
125
|
onChangeText={(text) => onChangeBox(key, text)}
|
|
106
126
|
onFocus={() => onFocusBox(key)}
|
|
107
127
|
onBlur={() => onBlurBox(key)}
|
|
@@ -110,9 +130,10 @@ const TypedTime: React.FC<Props> = ({
|
|
|
110
130
|
}}
|
|
111
131
|
style={[s.boxText, styles?.timeBoxText, s.boxInput]}
|
|
112
132
|
/>
|
|
133
|
+
<KeyboardAccessoryBar nativeID={`${barId}-${key}`} tokens={tokens} />
|
|
113
134
|
</View>
|
|
114
135
|
</View>
|
|
115
|
-
</
|
|
136
|
+
</Pressable>
|
|
116
137
|
);
|
|
117
138
|
|
|
118
139
|
return (
|