rn-grid-date-time-picker 1.2.0 → 1.2.2

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 (102) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/lib/commonjs/components/DateTimePicker.js +5 -0
  3. package/lib/commonjs/components/DateTimePicker.js.map +1 -1
  4. package/lib/commonjs/components/field/KeyboardAccessoryBar.js +38 -0
  5. package/lib/commonjs/components/field/KeyboardAccessoryBar.js.map +1 -0
  6. package/lib/commonjs/components/field/SegmentedInput.js +11 -6
  7. package/lib/commonjs/components/field/SegmentedInput.js.map +1 -1
  8. package/lib/commonjs/components/theme/defaultStrings.js.map +1 -1
  9. package/lib/commonjs/components/theme/styleKeys.js +3 -1
  10. package/lib/commonjs/components/theme/styleKeys.js.map +1 -1
  11. package/lib/commonjs/components/time/TimeRow.js +42 -4
  12. package/lib/commonjs/components/time/TimeRow.js.map +1 -1
  13. package/lib/commonjs/components/time/TimeRows.js +27 -1
  14. package/lib/commonjs/components/time/TimeRows.js.map +1 -1
  15. package/lib/commonjs/components/time/TypedTime.js +14 -8
  16. package/lib/commonjs/components/time/TypedTime.js.map +1 -1
  17. package/lib/commonjs/components/time/timeStyles.js +15 -1
  18. package/lib/commonjs/components/time/timeStyles.js.map +1 -1
  19. package/lib/commonjs/components/types.js +2 -0
  20. package/lib/commonjs/components/types.js.map +1 -1
  21. package/lib/commonjs/core/zoned.js +18 -10
  22. package/lib/commonjs/core/zoned.js.map +1 -1
  23. package/lib/module/components/DateTimePicker.js +5 -0
  24. package/lib/module/components/DateTimePicker.js.map +1 -1
  25. package/lib/module/components/field/KeyboardAccessoryBar.js +33 -0
  26. package/lib/module/components/field/KeyboardAccessoryBar.js.map +1 -0
  27. package/lib/module/components/field/SegmentedInput.js +11 -7
  28. package/lib/module/components/field/SegmentedInput.js.map +1 -1
  29. package/lib/module/components/theme/defaultStrings.js.map +1 -1
  30. package/lib/module/components/theme/styleKeys.js +3 -1
  31. package/lib/module/components/theme/styleKeys.js.map +1 -1
  32. package/lib/module/components/time/TimeRow.js +43 -5
  33. package/lib/module/components/time/TimeRow.js.map +1 -1
  34. package/lib/module/components/time/TimeRows.js +27 -1
  35. package/lib/module/components/time/TimeRows.js.map +1 -1
  36. package/lib/module/components/time/TypedTime.js +12 -8
  37. package/lib/module/components/time/TypedTime.js.map +1 -1
  38. package/lib/module/components/time/timeStyles.js +15 -1
  39. package/lib/module/components/time/timeStyles.js.map +1 -1
  40. package/lib/module/components/types.js +2 -0
  41. package/lib/module/components/types.js.map +1 -1
  42. package/lib/module/core/zoned.js +18 -10
  43. package/lib/module/core/zoned.js.map +1 -1
  44. package/lib/typescript/commonjs/src/components/DateTimePicker.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/src/components/field/KeyboardAccessoryBar.d.ts +15 -0
  46. package/lib/typescript/commonjs/src/components/field/KeyboardAccessoryBar.d.ts.map +1 -0
  47. package/lib/typescript/commonjs/src/components/field/SegmentedInput.d.ts +2 -2
  48. package/lib/typescript/commonjs/src/components/field/SegmentedInput.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/src/components/theme/defaultStrings.d.ts +1 -0
  50. package/lib/typescript/commonjs/src/components/theme/defaultStrings.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/src/components/theme/styleKeys.d.ts +1 -1
  52. package/lib/typescript/commonjs/src/components/theme/styleKeys.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/src/components/time/TimeRow.d.ts +4 -0
  54. package/lib/typescript/commonjs/src/components/time/TimeRow.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/src/components/time/TimeRows.d.ts +7 -0
  56. package/lib/typescript/commonjs/src/components/time/TimeRows.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/src/components/time/TypedTime.d.ts +1 -1
  58. package/lib/typescript/commonjs/src/components/time/TypedTime.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/src/components/time/timeStyles.d.ts +24 -1
  60. package/lib/typescript/commonjs/src/components/time/timeStyles.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/src/components/types.d.ts +21 -0
  62. package/lib/typescript/commonjs/src/components/types.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/src/core/zoned.d.ts +5 -1
  64. package/lib/typescript/commonjs/src/core/zoned.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/src/index.d.ts +1 -1
  66. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  67. package/lib/typescript/module/src/components/DateTimePicker.d.ts.map +1 -1
  68. package/lib/typescript/module/src/components/field/KeyboardAccessoryBar.d.ts +15 -0
  69. package/lib/typescript/module/src/components/field/KeyboardAccessoryBar.d.ts.map +1 -0
  70. package/lib/typescript/module/src/components/field/SegmentedInput.d.ts +2 -2
  71. package/lib/typescript/module/src/components/field/SegmentedInput.d.ts.map +1 -1
  72. package/lib/typescript/module/src/components/theme/defaultStrings.d.ts +1 -0
  73. package/lib/typescript/module/src/components/theme/defaultStrings.d.ts.map +1 -1
  74. package/lib/typescript/module/src/components/theme/styleKeys.d.ts +1 -1
  75. package/lib/typescript/module/src/components/theme/styleKeys.d.ts.map +1 -1
  76. package/lib/typescript/module/src/components/time/TimeRow.d.ts +4 -0
  77. package/lib/typescript/module/src/components/time/TimeRow.d.ts.map +1 -1
  78. package/lib/typescript/module/src/components/time/TimeRows.d.ts +7 -0
  79. package/lib/typescript/module/src/components/time/TimeRows.d.ts.map +1 -1
  80. package/lib/typescript/module/src/components/time/TypedTime.d.ts +1 -1
  81. package/lib/typescript/module/src/components/time/TypedTime.d.ts.map +1 -1
  82. package/lib/typescript/module/src/components/time/timeStyles.d.ts +24 -1
  83. package/lib/typescript/module/src/components/time/timeStyles.d.ts.map +1 -1
  84. package/lib/typescript/module/src/components/types.d.ts +21 -0
  85. package/lib/typescript/module/src/components/types.d.ts.map +1 -1
  86. package/lib/typescript/module/src/core/zoned.d.ts +5 -1
  87. package/lib/typescript/module/src/core/zoned.d.ts.map +1 -1
  88. package/lib/typescript/module/src/index.d.ts +1 -1
  89. package/lib/typescript/module/src/index.d.ts.map +1 -1
  90. package/package.json +1 -1
  91. package/src/components/DateTimePicker.tsx +5 -0
  92. package/src/components/field/KeyboardAccessoryBar.tsx +38 -0
  93. package/src/components/field/SegmentedInput.tsx +12 -7
  94. package/src/components/theme/defaultStrings.ts +1 -0
  95. package/src/components/theme/styleKeys.ts +4 -0
  96. package/src/components/time/TimeRow.tsx +49 -2
  97. package/src/components/time/TimeRows.tsx +38 -2
  98. package/src/components/time/TypedTime.tsx +7 -5
  99. package/src/components/time/timeStyles.ts +10 -1
  100. package/src/components/types.ts +22 -0
  101. package/src/core/zoned.ts +15 -3
  102. package/src/index.tsx +1 -0
@@ -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 a "Done" bar on iOS (its number pad has no
97
- * return key).
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 adds a Done button above the number pad for every box; it
201
- // closes the keyboard (and blurs the box).
202
- returnKeyType="done"
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
  };
@@ -8,6 +8,7 @@ export interface PickerStrings {
8
8
  reset: string;
9
9
  clear: string;
10
10
  today: string;
11
+ /** No longer shown (the iOS number-pad bar is empty); kept so existing `strings` objects still type-check. */
11
12
  done: string;
12
13
  time: string;
13
14
  startTime: string;
@@ -110,6 +110,10 @@ export const STYLE_KEYS = [
110
110
  // time
111
111
  'timeRow',
112
112
  'timeLabel',
113
+ /** Holds the label and its date (`timeRowDate`) or a `renderTimeLabel` label. */
114
+ 'timeLabelWrap',
115
+ /** The picked day under a time row's label (`timeRowDate`). */
116
+ 'timeLabelDate',
113
117
  'timeBox',
114
118
  'timeBoxFocused',
115
119
  'timeBoxError',
@@ -57,6 +57,10 @@ export interface TimeRowProps {
57
57
  /** The day's limits (`Constraints.timeBounds`). */
58
58
  bounds?: TimeBounds | null;
59
59
  label: string;
60
+ /** The row's day, shown under the label (`timeRowDate`); null shows none. */
61
+ dateText?: string | null;
62
+ /** Replaces the label and its date (`renderTimeLabel`). */
63
+ renderLabel?: () => React.ReactNode;
60
64
  disabled?: boolean;
61
65
  tokens: ThemeTokens;
62
66
  styles?: PickerStyles;
@@ -77,6 +81,37 @@ const issueText = (
77
81
  return strings.errorTimeNotAllowed;
78
82
  };
79
83
 
84
+ /** The row's label: plain, with the day under it, or the app's own. */
85
+ const TimeLabel: React.FC<
86
+ Pick<
87
+ TimeRowProps,
88
+ 'label' | 'dateText' | 'renderLabel' | 'tokens' | 'styles' | 'testID'
89
+ >
90
+ > = ({ label, dateText, renderLabel, tokens, styles, testID }) => {
91
+ const s = buildTimeStyles(tokens);
92
+ if (!renderLabel && !dateText)
93
+ return <Text style={[s.label, styles?.timeLabel]}>{label}</Text>;
94
+ return (
95
+ <View style={[s.labelWrap, styles?.timeLabelWrap]}>
96
+ {renderLabel ? (
97
+ renderLabel()
98
+ ) : (
99
+ <>
100
+ <Text style={[s.label, s.labelInWrap, styles?.timeLabel]}>
101
+ {label}
102
+ </Text>
103
+ <Text
104
+ style={[s.labelDate, styles?.timeLabelDate]}
105
+ testID={testID ? `${testID}-date` : undefined}
106
+ >
107
+ {dateText}
108
+ </Text>
109
+ </>
110
+ )}
111
+ </View>
112
+ );
113
+ };
114
+
80
115
  /**
81
116
  * Label, the time boxes and the error below them. The boxes are
82
117
  * normal inputs (free cursor, digits only, two at most) with three helpers:
@@ -97,6 +132,8 @@ const TimeRow: React.FC<TimeRowProps> = ({
97
132
  minuteStep = 1,
98
133
  bounds,
99
134
  label,
135
+ dateText,
136
+ renderLabel,
100
137
  disabled,
101
138
  tokens,
102
139
  styles,
@@ -205,8 +242,11 @@ const TimeRow: React.FC<TimeRowProps> = ({
205
242
  let issue: TimeEntryIssue | null = effective.issue;
206
243
  if (effective.isEmpty) issue = time ? 'INCOMPLETE' : null;
207
244
  if (effective.time) issue = checkTime(effective.time, { minuteStep, bounds });
245
+ // A time set elsewhere reaches the boxes one render later (the effect
246
+ // above); until then the boxes are stale, so check that time instead.
247
+ const boxesStale = !focused && timeKey(time) !== lastKey.current;
208
248
  // The wheel always holds a complete time: only the limits and the step apply.
209
- if (input === 'wheel')
249
+ if (input === 'wheel' || boxesStale)
210
250
  issue = time ? checkTime(time, { minuteStep, bounds }) : null;
211
251
 
212
252
  const pending =
@@ -268,7 +308,14 @@ const TimeRow: React.FC<TimeRowProps> = ({
268
308
  return (
269
309
  <View style={s.wrapper} testID={testID}>
270
310
  <View style={[s.row, styles?.timeRow]}>
271
- <Text style={[s.label, styles?.timeLabel]}>{label}</Text>
311
+ <TimeLabel
312
+ label={label}
313
+ dateText={dateText}
314
+ renderLabel={renderLabel}
315
+ tokens={tokens}
316
+ styles={styles}
317
+ testID={testID}
318
+ />
272
319
  {input === 'wheel' ? (
273
320
  <TimeWheel
274
321
  time={time}
@@ -5,10 +5,11 @@ import type { Draft, DraftTarget } from '../../core/draft';
5
5
  import type { TimeEntryIssue } from '../../core/timeEntry';
6
6
  import type { CalendarDate, Time, TimeFormat } from '../../core/types';
7
7
  import type { RangeEnd } from '../field/RangeFields';
8
- import { formatDateText } from '../field/fieldText';
8
+ import { formatDateText, type DateTextFormatter } from '../field/fieldText';
9
9
  import type { PickerStrings } from '../theme/defaultStrings';
10
10
  import type { PickerStyles } from '../theme/styleKeys';
11
11
  import type { ThemeTokens } from '../theme/tokens';
12
+ import type { TimeLabelRenderProps } from '../types';
12
13
  import TimeRow from './TimeRow';
13
14
 
14
15
  /** Times typed before their day is picked, per end ('shared' lives in the draft). */
@@ -37,6 +38,11 @@ interface Props {
37
38
  onSpinChange: (spinning: boolean) => void;
38
39
  /** A row's box gained or lost focus. */
39
40
  onFocusChange?: (rowKey: string, focused: boolean) => void;
41
+ /** Shows each row's day under its label. */
42
+ timeRowDate?: boolean;
43
+ renderTimeLabel?: (p: TimeLabelRenderProps) => React.ReactNode;
44
+ /** A day as the field writes it (the Hijri text for a Hijri pick); default `dateFormat`. */
45
+ dateText?: DateTextFormatter;
40
46
  /** A row's time that is typed but not saved yet (null once saved or gone). */
41
47
  onPendingChange?: (
42
48
  rowKey: string,
@@ -51,6 +57,7 @@ interface Props {
51
57
 
52
58
  interface RowSpec {
53
59
  key: string;
60
+ kind: TimeLabelRenderProps['row'];
54
61
  label: string;
55
62
  time: Time | null;
56
63
  day: CalendarDate | null;
@@ -85,6 +92,9 @@ const TimeRows: React.FC<Props> = (props) => {
85
92
  onSpinChange,
86
93
  onFocusChange,
87
94
  onPendingChange,
95
+ timeRowDate = false,
96
+ renderTimeLabel,
97
+ dateText,
88
98
  tokens,
89
99
  styles,
90
100
  strings,
@@ -98,6 +108,7 @@ const TimeRows: React.FC<Props> = (props) => {
98
108
  ): RowSpec => {
99
109
  return {
100
110
  key,
111
+ kind: key,
101
112
  label,
102
113
  time: value?.time ?? pending[key] ?? null,
103
114
  day: value?.date ?? null,
@@ -123,6 +134,7 @@ const TimeRows: React.FC<Props> = (props) => {
123
134
  rows = [
124
135
  {
125
136
  key: 'shared',
137
+ kind: 'shared',
126
138
  label: strings.time,
127
139
  time: draft.sharedTime,
128
140
  day: null,
@@ -142,7 +154,11 @@ const TimeRows: React.FC<Props> = (props) => {
142
154
  rows = [
143
155
  {
144
156
  key: `day-${slot.date.year}-${slot.date.month}-${slot.date.day}`,
145
- label: `${strings.time} · ${formatDateText(slot, dateFormat)}`,
157
+ kind: 'day',
158
+ // With timeRowDate the day goes under the label instead.
159
+ label: timeRowDate
160
+ ? strings.time
161
+ : `${strings.time} · ${formatDateText(slot, dateFormat)}`,
146
162
  time: slot.time,
147
163
  day: slot.date,
148
164
  target: { day: slot.date },
@@ -200,6 +216,24 @@ const TimeRows: React.FC<Props> = (props) => {
200
216
  // eslint-disable-next-line react-hooks/exhaustive-deps -- reports changes only; the callback may be a new function on every render
201
217
  useEffect(() => onSpinChange(anySpinning), [anySpinning]);
202
218
 
219
+ /** A row's day as the field writes it, or null before it is picked. */
220
+ const dayText = (row: RowSpec): string | null => {
221
+ if (!row.day) return null;
222
+ const value = { date: row.day, time: row.time };
223
+ return dateText ? dateText(value) : formatDateText(value, dateFormat);
224
+ };
225
+ const labelRenderer = (row: RowSpec) =>
226
+ renderTimeLabel
227
+ ? () =>
228
+ renderTimeLabel({
229
+ label: row.label,
230
+ row: row.kind,
231
+ date: row.day,
232
+ dateText: dayText(row),
233
+ time: row.time,
234
+ })
235
+ : undefined;
236
+
203
237
  const single = rows.length === 1;
204
238
  return (
205
239
  <>
@@ -218,6 +252,8 @@ const TimeRows: React.FC<Props> = (props) => {
218
252
  minuteStep={minuteStep}
219
253
  bounds={row.day ? constraints.timeBounds(row.day) : null}
220
254
  label={row.label}
255
+ dateText={timeRowDate ? dayText(row) : null}
256
+ renderLabel={labelRenderer(row)}
221
257
  disabled={disabled}
222
258
  tokens={tokens}
223
259
  styles={styles}
@@ -1,5 +1,6 @@
1
- import React from 'react';
1
+ import React, { useId } from 'react';
2
2
  import { 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 Done bar
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,6 +62,7 @@ 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);
65
67
 
66
68
  const box = (key: TimeKey) => (
@@ -99,9 +101,8 @@ const TypedTime: React.FC<Props> = ({
99
101
  contextMenuHidden
100
102
  editable={!disabled}
101
103
  underlineColorAndroid="transparent"
102
- // iOS adds a Done button above the number pad for each box.
103
- returnKeyType="done"
104
- inputAccessoryViewButtonLabel={strings.done}
104
+ // iOS: this box's own solid bar (below).
105
+ inputAccessoryViewID={`${barId}-${key}`}
105
106
  onChangeText={(text) => onChangeBox(key, text)}
106
107
  onFocus={() => onFocusBox(key)}
107
108
  onBlur={() => onBlurBox(key)}
@@ -110,6 +111,7 @@ const TypedTime: React.FC<Props> = ({
110
111
  }}
111
112
  style={[s.boxText, styles?.timeBoxText, s.boxInput]}
112
113
  />
114
+ <KeyboardAccessoryBar nativeID={`${barId}-${key}`} tokens={tokens} />
113
115
  </View>
114
116
  </View>
115
117
  </View>
@@ -23,6 +23,15 @@ const create = (t: ThemeTokens) =>
23
23
  fontSize: t.fontSizeLarge,
24
24
  color: t.heading,
25
25
  },
26
+ // The label with the day under it (timeRowDate), or a renderTimeLabel label.
27
+ labelWrap: { flexShrink: 1, marginEnd: 12 },
28
+ labelInWrap: { marginEnd: 0 },
29
+ labelDate: {
30
+ marginTop: 2,
31
+ ...fontStyle(t, 'regular'),
32
+ fontSize: t.fontSizeSmall,
33
+ color: t.mutedText,
34
+ },
26
35
  boxes: {
27
36
  flexDirection: 'row',
28
37
  alignItems: 'center',
@@ -62,9 +71,9 @@ const create = (t: ThemeTokens) =>
62
71
  },
63
72
  boxFocused: { borderColor: t.borderFocused },
64
73
  boxError: { borderColor: t.error },
74
+ // No height of its own, so `timeBox` sets the row's height.
65
75
  colonCell: {
66
76
  width: 24,
67
- height: 46,
68
77
  alignItems: 'center',
69
78
  justifyContent: 'center',
70
79
  },
@@ -165,6 +165,20 @@ export interface LabelRenderProps {
165
165
  fieldTestID?: string;
166
166
  }
167
167
 
168
+ /** What `renderTimeLabel` receives, for one time row. */
169
+ export interface TimeLabelRenderProps {
170
+ /** The row's label from `strings`: 'Time', 'Start time' or 'End time'. */
171
+ label: string;
172
+ /** A single value's row, a range end, the multiple-mode shared row, or a day's own row (`timeMode: 'perDate'`). */
173
+ row: 'single' | 'from' | 'to' | 'shared' | 'day';
174
+ /** The row's picked day; null before it is picked, and for the shared row. */
175
+ date: CalendarDate | null;
176
+ /** That day as the field writes it; null when there is no day. Set whether or not `timeRowDate` is on. */
177
+ dateText: string | null;
178
+ /** The row's time, or null. */
179
+ time: Time | null;
180
+ }
181
+
168
182
  /** What `renderInstruction` receives. */
169
183
  export interface InstructionRenderProps {
170
184
  instruction: string;
@@ -368,6 +382,14 @@ export interface DateTimePickerProps {
368
382
  minuteStep?: number;
369
383
  /** A newly picked day always gets this time (over a time typed first and over the current time). */
370
384
  newDayTime?: Time;
385
+ /**
386
+ * Shows each time row's picked day under its label ('Start time',
387
+ * 10-03-2026), as the field writes it. Nothing shows until the day is
388
+ * picked. Style it with `timeLabelDate`. Default false.
389
+ */
390
+ timeRowDate?: boolean;
391
+ /** Replaces a time row's label (and its date). The boxes and errors stay built in. */
392
+ renderTimeLabel?: (p: TimeLabelRenderProps) => React.ReactNode;
371
393
 
372
394
  // limits → greyed days and errors
373
395
  showHolidays?: boolean;
package/src/core/zoned.ts CHANGED
@@ -1,9 +1,16 @@
1
1
  import { DateTime } from 'luxon';
2
2
  import type { DateTimeValue } from './types';
3
3
 
4
- /** The Luxon DateTime for a date-time value in `zone` (seconds and ms zeroed). */
5
- export const toZonedDateTime = (value: DateTimeValue, zone: string): DateTime =>
6
- DateTime.fromObject(
4
+ /**
5
+ * The Luxon DateTime for a date-time value in `zone` (seconds and ms zeroed).
6
+ * A time that happens twice resolves to its first occurrence: Luxon alone picks
7
+ * by the zone's offset today, so the result would change with the season.
8
+ */
9
+ export const toZonedDateTime = (
10
+ value: DateTimeValue,
11
+ zone: string
12
+ ): DateTime => {
13
+ const dt = DateTime.fromObject(
7
14
  {
8
15
  ...value.date,
9
16
  hour: value.time?.hour ?? 0,
@@ -13,6 +20,11 @@ export const toZonedDateTime = (value: DateTimeValue, zone: string): DateTime =>
13
20
  },
14
21
  { zone }
15
22
  );
23
+ if (!dt.isValid) return dt;
24
+ return dt
25
+ .getPossibleOffsets()
26
+ .reduce((first, d) => (d.toMillis() < first.toMillis() ? d : first), dt);
27
+ };
16
28
 
17
29
  export type DstStatus = 'ok' | 'gap' | 'overlap';
18
30
 
package/src/index.tsx CHANGED
@@ -70,6 +70,7 @@ export type {
70
70
  SheetRenderProps,
71
71
  SheetSize,
72
72
  Slot,
73
+ TimeLabelRenderProps,
73
74
  } from './components/types';
74
75
  export type { CalendarSystem, Periods } from './core/calendarSystem';
75
76
  export type { Numerals } from './core/numerals';