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.
- package/CHANGELOG.md +35 -0
- package/lib/commonjs/components/DateTimePicker.js +109 -129
- package/lib/commonjs/components/DateTimePicker.js.map +1 -1
- package/lib/commonjs/components/calendar/DayCell.js +17 -6
- package/lib/commonjs/components/calendar/DayCell.js.map +1 -1
- package/lib/commonjs/components/calendar/calendarStyles.js +11 -0
- package/lib/commonjs/components/calendar/calendarStyles.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/ValueField.js +19 -2
- package/lib/commonjs/components/field/ValueField.js.map +1 -1
- package/lib/commonjs/components/field/fieldText.js +44 -5
- package/lib/commonjs/components/field/fieldText.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/styleKeys.js +3 -1
- package/lib/commonjs/components/theme/styleKeys.js.map +1 -1
- package/lib/commonjs/components/time/TypedTime.js +20 -2
- package/lib/commonjs/components/time/TypedTime.js.map +1 -1
- package/lib/commonjs/components/types.js +4 -1
- 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 +110 -130
- package/lib/module/components/DateTimePicker.js.map +1 -1
- package/lib/module/components/calendar/DayCell.js +18 -7
- package/lib/module/components/calendar/DayCell.js.map +1 -1
- package/lib/module/components/calendar/calendarStyles.js +11 -0
- package/lib/module/components/calendar/calendarStyles.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/ValueField.js +20 -3
- package/lib/module/components/field/ValueField.js.map +1 -1
- package/lib/module/components/field/fieldText.js +40 -4
- package/lib/module/components/field/fieldText.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/styleKeys.js +3 -1
- package/lib/module/components/theme/styleKeys.js.map +1 -1
- package/lib/module/components/time/TypedTime.js +22 -4
- package/lib/module/components/time/TypedTime.js.map +1 -1
- package/lib/module/components/types.js +4 -1
- 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/calendar/DayCell.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/components/calendar/calendarStyles.d.ts +9 -0
- package/lib/typescript/commonjs/src/components/calendar/calendarStyles.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/ValueField.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/components/field/fieldText.d.ts +19 -0
- package/lib/typescript/commonjs/src/components/field/fieldText.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/styleKeys.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/components/time/TypedTime.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/components/types.d.ts +59 -8
- 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/calendar/DayCell.d.ts.map +1 -1
- package/lib/typescript/module/src/components/calendar/calendarStyles.d.ts +9 -0
- package/lib/typescript/module/src/components/calendar/calendarStyles.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/ValueField.d.ts.map +1 -1
- package/lib/typescript/module/src/components/field/fieldText.d.ts +19 -0
- package/lib/typescript/module/src/components/field/fieldText.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/styleKeys.d.ts.map +1 -1
- package/lib/typescript/module/src/components/time/TypedTime.d.ts.map +1 -1
- package/lib/typescript/module/src/components/types.d.ts +59 -8
- 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 +141 -135
- package/src/components/calendar/DayCell.tsx +25 -6
- package/src/components/calendar/calendarStyles.ts +11 -0
- package/src/components/container/PickerContainer.tsx +1 -0
- package/src/components/container/PickerLayout.tsx +10 -1
- package/src/components/field/ValueField.tsx +30 -11
- package/src/components/field/fieldText.ts +48 -5
- package/src/components/hooks/useScrollToTime.ts +99 -0
- package/src/components/nativeTypes.ts +2 -1
- package/src/components/theme/styleKeys.ts +3 -1
- package/src/components/time/TypedTime.tsx +25 -6
- package/src/components/types.ts +63 -8
- package/src/core/draft.ts +5 -2
- 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
|
|
39
|
-
* disabled, holiday colour. Only
|
|
40
|
-
* memo keeps typing and taps
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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}${
|
|
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'
|
|
123
|
-
export const
|
|
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
|
|
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
|
-
<
|
|
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) =>
|
|
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
|
-
</
|
|
136
|
+
</Pressable>
|
|
118
137
|
);
|
|
119
138
|
|
|
120
139
|
return (
|
package/src/components/types.ts
CHANGED
|
@@ -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
|
|
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
|
|
340
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
+
const fallback =
|
|
162
|
+
opts.defaultTime === undefined ? DEFAULT_TIME : opts.defaultTime;
|
|
163
|
+
return existing ?? newDayTime ?? fallback;
|
|
161
164
|
};
|
|
162
165
|
|
|
163
166
|
const withDay = (
|