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
@@ -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 },
@@ -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 { PickerState, Slot } from '../types';
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}
@@ -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>
@@ -119,27 +119,70 @@ export const formatMultipleText = (
119
119
  )
120
120
  .join(', ');
121
121
 
122
- /** 'DD-MM-YYYY' or 'DD-MM-YYYY hh:mm AM', from the placeholder strings. */
123
- export const placeholderText = (
122
+ /** { date: 'DD-MM-YYYY', time: 'hh:mm AM' } (time '' unless datetime), from the placeholder strings. */
123
+ export const placeholderParts = (
124
124
  type: PickerType,
125
125
  dateFormat: string,
126
126
  timeFormat: TimeFormat,
127
127
  strings: PickerStrings = DEFAULT_STRINGS
128
- ): string => {
128
+ ): { date: string; time: string } => {
129
129
  const parts: Record<string, string> = {
130
130
  dd: strings.placeholderDay,
131
131
  MM: strings.placeholderMonth,
132
132
  yyyy: strings.placeholderYear,
133
133
  };
134
134
  const date = dateFormat.replace(/yyyy|MM|dd/g, (t) => parts[t] ?? t);
135
- if (type !== 'datetime') return date;
135
+ if (type !== 'datetime') return { date, time: '' };
136
136
  const time =
137
137
  timeFormat === '24h'
138
138
  ? `${strings.placeholderHour24}:${strings.placeholderMinute}`
139
139
  : `${strings.placeholderHour}:${strings.placeholderMinute} ${strings.am}`;
140
- return `${date}${strings.dateTimeSeparator ?? ' '}${time}`;
140
+ return { date, time };
141
141
  };
142
142
 
143
+ /** 'DD-MM-YYYY' or 'DD-MM-YYYY hh:mm AM', from the placeholder strings. */
144
+ export const placeholderText = (
145
+ type: PickerType,
146
+ dateFormat: string,
147
+ timeFormat: TimeFormat,
148
+ strings: PickerStrings = DEFAULT_STRINGS
149
+ ): string => {
150
+ const { date, time } = placeholderParts(
151
+ type,
152
+ dateFormat,
153
+ timeFormat,
154
+ strings
155
+ );
156
+ return time ? `${date}${strings.dateTimeSeparator ?? ' '}${time}` : date;
157
+ };
158
+
159
+ /**
160
+ * A placeholder passed in, cut at the first date-time separator into the
161
+ * date and the time ('DD/MM/YYYY HH:MM' -> 'DD/MM/YYYY', 'HH:MM'). Text
162
+ * whose second part isn't a time (no ':'), like 'Pick a time', stays whole.
163
+ */
164
+ export const splitPlaceholder = (
165
+ text: string,
166
+ separator: string
167
+ ): { date: string; time: string } => {
168
+ const at = separator ? text.indexOf(separator) : -1;
169
+ const time = at > 0 ? text.slice(at + separator.length) : '';
170
+ if (!time.includes(':')) return { date: text, time: '' };
171
+ return { date: text.slice(0, at), time };
172
+ };
173
+
174
+ /** The empty field's placeholder (the one passed in, else the built one) as date and time. */
175
+ export const emptyFieldParts = (
176
+ placeholder: string | undefined,
177
+ type: PickerType,
178
+ dateFormat: string,
179
+ timeFormat: TimeFormat,
180
+ strings: PickerStrings = DEFAULT_STRINGS
181
+ ): { date: string; time: string } =>
182
+ placeholder
183
+ ? splitPlaceholder(placeholder, strings.dateTimeSeparator ?? ' ')
184
+ : placeholderParts(type, dateFormat, timeFormat, strings);
185
+
143
186
  const TIME_KEYS = ['hour', 'minute', 'period'];
144
187
 
145
188
  /** The message shown below the field for the first typed-input problem. */
@@ -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];
@@ -27,7 +27,9 @@ export const STYLE_KEYS = [
27
27
  'fieldTextDisabled',
28
28
  /**
29
29
  * Sets the date of a single date-time value apart from its time, e.g.
30
- * `{ minWidth: '50%' }` starts the time at the middle of the field.
30
+ * `{ minWidth: '50%' }` starts the time at the middle of the field. The empty
31
+ * field's placeholder does the same: a passed `placeholder` is cut at the
32
+ * first `strings.dateTimeSeparator` when what follows is a time ('HH:MM').
31
33
  */
32
34
  'fieldDateText',
33
35
  'fieldPlaceholder',
@@ -1,5 +1,5 @@
1
- import React, { useId } 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
3
  import KeyboardAccessoryBar from '../field/KeyboardAccessoryBar';
4
4
  import type { TextInputRef } from '../nativeTypes';
5
5
  import type { TimeBounds } from '../../core/constraints';
@@ -64,9 +64,25 @@ const TypedTime: React.FC<Props> = ({
64
64
  const digits = useNumerals();
65
65
  const barId = useId();
66
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;
67
76
 
68
77
  const box = (key: TimeKey) => (
69
- <View style={s.boxWrap}>
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
+ >
70
86
  <View
71
87
  style={[
72
88
  s.boxContainer,
@@ -75,9 +91,12 @@ const TypedTime: React.FC<Props> = ({
75
91
  hasError && [s.boxError, styles?.timeBoxError],
76
92
  ]}
77
93
  >
78
- <View style={s.boxInner}>
94
+ <View style={s.boxInner} pointerEvents={passTouches ? 'none' : 'auto'}>
79
95
  <TextInput
80
- ref={(r) => setRef(key, r)}
96
+ ref={(r) => {
97
+ inputs.current[key] = r;
98
+ setRef(key, r);
99
+ }}
81
100
  testID={testID ? `${testID}-${key}` : undefined}
82
101
  accessibilityLabel={
83
102
  key === 'hour' ? strings.a11yHour : strings.a11yMinute
@@ -114,7 +133,7 @@ const TypedTime: React.FC<Props> = ({
114
133
  <KeyboardAccessoryBar nativeID={`${barId}-${key}`} tokens={tokens} />
115
134
  </View>
116
135
  </View>
117
- </View>
136
+ </Pressable>
118
137
  );
119
138
 
120
139
  return (
@@ -1,8 +1,9 @@
1
1
  import type React from 'react';
2
- import type { StyleProp, TextStyle } from 'react-native';
2
+ import type { ScrollViewProps, StyleProp, TextStyle } from 'react-native';
3
3
  import type { CalendarSystem } from '../core/calendarSystem';
4
4
  import type { Constraints, TimeBounds } from '../core/constraints';
5
5
  import type { Numerals } from '../core/numerals';
6
+ import type { ScrollViewRef } from './nativeTypes';
6
7
  import type {
7
8
  CalendarDate,
8
9
  PickerType,
@@ -130,11 +131,40 @@ export interface SheetOptions {
130
131
  fieldPlaceholder?: string;
131
132
  /** The calendar icon at the end of that field, as on the on-screen field. Default false. */
132
133
  fieldIcon?: boolean;
134
+ /**
135
+ * "This date isn't available" above the calendar while the picked date (or a
136
+ * range's end) is a disabled day, e.g. a saved value the rules no longer
137
+ * allow. false hides it; Confirm stays off for that day. Default true;
138
+ * applies inline too.
139
+ */
140
+ showNotAllowedError?: boolean;
141
+ /**
142
+ * Passed to the sheet body's ScrollView, over the picker's own defaults, e.g.
143
+ * `{ showsVerticalScrollIndicator: true, persistentScrollbar: true }` to keep
144
+ * the scrollbar showing on Android (iOS shows it only while scrolling).
145
+ * Applies to the built-in sheet and to `renderSheet`.
146
+ */
147
+ scrollProps?: SheetScrollProps;
148
+ /**
149
+ * Date-time sheets: once a calendar tap picks the date (a range's end, a
150
+ * multiple picker's first date), the body scrolls the time section into view
151
+ * when it doesn't show in full. true: just far enough to show it; 'end': to
152
+ * the end of the body; false: no scroll. Default true. Applies to the
153
+ * built-in sheet and to `renderSheet`.
154
+ */
155
+ scrollToTime?: boolean | 'end';
133
156
  }
134
157
 
158
+ /** ScrollView props a sheet's body takes (`sheet.scrollProps`). */
159
+ export type SheetScrollProps = Omit<
160
+ ScrollViewProps,
161
+ 'children' | 'style' | 'testID'
162
+ >;
163
+
135
164
  /**
136
165
  * The second argument of `onChange` when the picker can show a Hijri calendar:
137
- * the calendar the date was picked in. The value itself is always Gregorian.
166
+ * the calendar the date was picked in, which is the tab on screen when it was
167
+ * saved. The value itself is always Gregorian.
138
168
  * The picker remembers it while mounted; to show a stored value back the same
139
169
  * way later, store this too and pass it as `dualDefaultTab` (dual) or
140
170
  * `calendarSystem`.
@@ -243,6 +273,13 @@ export interface DateTimePickerProps {
243
273
 
244
274
  // presentation
245
275
  presentation?: 'sheet' | 'inline';
276
+ /**
277
+ * Inline date-time pickers: your ScrollView around the picker. Once a
278
+ * calendar tap picks the date, it scrolls the time section into view, as
279
+ * `sheet.scrollToTime: true` does in the sheet. Without it an inline picker
280
+ * never scrolls.
281
+ */
282
+ scrollToTimeRef?: React.RefObject<ScrollViewRef | null>;
246
283
  visible?: boolean;
247
284
  onOpen?: () => void;
248
285
  onClose?: () => void;
@@ -336,9 +373,9 @@ export interface DateTimePickerProps {
336
373
  showToday?: boolean;
337
374
  /**
338
375
  * The calendar the grid uses: 'gregory', 'hijri' (Umm al-Qura), or 'dual':
339
- * Gregorian and Hijri tabs, one calendar at a time. A day is shown selected
340
- * only in the calendar it was picked in, and the field writes it in that
341
- * calendar. With `calendarSwitch`, this is the one shown first. Default
376
+ * Gregorian and Hijri tabs, one calendar at a time. A selection shows in
377
+ * both tabs and is saved with the tab on screen, and the field writes it in
378
+ * that calendar. With `calendarSwitch`, this is the one shown first. Default
342
379
  * 'gregory'; anything Hijri falls back to 'gregory' when the platform has no
343
380
  * Hijri calendar.
344
381
  */
@@ -357,8 +394,21 @@ export interface DateTimePickerProps {
357
394
  * readers say. Default 'ar-SA' (Arabic).
358
395
  */
359
396
  hijriLocale?: string;
360
- /** The tab a dual calendar opens on, each time it opens. Default 'gregory'. */
397
+ /**
398
+ * The tab a dual calendar opens on when it has no value. With a value, it
399
+ * opens on the tab that value was saved with (remembered while the picker is
400
+ * mounted; a value from outside counts as saved with this tab). Default
401
+ * 'gregory'.
402
+ */
361
403
  dualDefaultTab?: 'gregory' | 'hijri';
404
+ /**
405
+ * How the on-screen field writes the saved value. 'picked' (default): in the
406
+ * calendar it was picked in, so a Hijri pick shows Hijri text, digits and
407
+ * AM / PM. 'gregory': always in `dateFormat`, with `strings` and the
408
+ * Gregorian digits. The field inside the sheet and the time rows still
409
+ * follow the tab.
410
+ */
411
+ fieldCalendar?: 'picked' | 'gregory';
362
412
  /** Replaces the built-in calendar switch. */
363
413
  renderCalendarSwitch?: (p: CalendarSwitchRenderProps) => React.ReactNode;
364
414
  /** Any BCP-47 tag; decides names, the date order and the clock. Default: the device locale. */
@@ -380,8 +430,13 @@ export interface DateTimePickerProps {
380
430
  /** Default: from `locale`, else '12h'. */
381
431
  timeFormat?: TimeFormat;
382
432
  minuteStep?: number;
383
- /** A newly picked day always gets this time (over a time typed first and over the current time). */
384
- newDayTime?: Time;
433
+ /**
434
+ * The time a newly picked day gets (a saved time is kept). 'now' (default):
435
+ * the current time. A `Time`: always that time, over a time typed first.
436
+ * 'empty': none; the time boxes stay empty and Confirm (and 'instant'
437
+ * saving) waits until the user enters the time.
438
+ */
439
+ newDayTime?: Time | 'now' | 'empty';
385
440
  /**
386
441
  * Shows each time row's picked day under its label ('Start time',
387
442
  * 10-03-2026), as the field writes it. Nothing shows until the day is
package/src/core/draft.ts CHANGED
@@ -53,7 +53,8 @@ export interface DraftOptions {
53
53
  zone: string;
54
54
  /** Turns saved strings into values and back. */
55
55
  adapter: ValueAdapter;
56
- defaultTime?: Time;
56
+ /** Time for a newly picked day when none is given; null leaves it empty. Default DEFAULT_TIME. */
57
+ defaultTime?: Time | null;
57
58
  multiple?: {
58
59
  timeMode?: 'shared' | 'perDate';
59
60
  maxSelections?: number;
@@ -157,7 +158,9 @@ const timeForNewDay = (
157
158
  newDayTime?: Time | null
158
159
  ): Time | null => {
159
160
  if (opts.type !== 'datetime') return null;
160
- return existing ?? newDayTime ?? opts.defaultTime ?? DEFAULT_TIME;
161
+ const fallback =
162
+ opts.defaultTime === undefined ? DEFAULT_TIME : opts.defaultTime;
163
+ return existing ?? newDayTime ?? fallback;
161
164
  };
162
165
 
163
166
  const withDay = (
package/src/index.tsx CHANGED
@@ -68,6 +68,7 @@ export type {
68
68
  PickerState,
69
69
  SheetOptions,
70
70
  SheetRenderProps,
71
+ SheetScrollProps,
71
72
  SheetSize,
72
73
  Slot,
73
74
  TimeLabelRenderProps,