@astryxdesign/core 0.7.8-canary.ffd29c5 → 0.7.8

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 (189) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/dist/BottomSheet/BottomSheetPanel.d.ts +1 -1
  3. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetPanel.js +30 -14
  5. package/dist/BottomSheet/bottomSheetStack.js +1 -1
  6. package/dist/BottomSheet/useSheetGestures.d.ts +3 -1
  7. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  8. package/dist/BottomSheet/useSheetGestures.js +4 -0
  9. package/dist/CheckboxList/CheckboxList.d.ts +13 -2
  10. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  11. package/dist/CheckboxList/CheckboxList.js +10 -4
  12. package/dist/CheckboxList/CheckboxListContext.d.ts +7 -0
  13. package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
  14. package/dist/CheckboxList/CheckboxListContext.js +2 -1
  15. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  16. package/dist/CheckboxList/CheckboxListItem.js +14 -2
  17. package/dist/CheckboxList/requiredGroup.d.ts +22 -0
  18. package/dist/CheckboxList/requiredGroup.d.ts.map +1 -0
  19. package/dist/CheckboxList/requiredGroup.js +52 -0
  20. package/dist/DateInput/PickerWheel.d.ts +1 -1
  21. package/dist/DateInput/PickerWheel.d.ts.map +1 -1
  22. package/dist/DateInput/PickerWheel.js +27 -6
  23. package/dist/DateInput/Wheel.d.ts +2 -2
  24. package/dist/DateInput/Wheel.d.ts.map +1 -1
  25. package/dist/DateInput/Wheel.js +20 -6
  26. package/dist/DateInput/datePickerTouch.d.ts +13 -0
  27. package/dist/DateInput/datePickerTouch.d.ts.map +1 -0
  28. package/dist/DateInput/datePickerTouch.js +14 -0
  29. package/dist/DateInput/wheelOptions.d.ts +22 -2
  30. package/dist/DateInput/wheelOptions.d.ts.map +1 -1
  31. package/dist/DateInput/wheelOptions.js +14 -2
  32. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  33. package/dist/DateTimeInput/TouchDateTimeField.js +62 -119
  34. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  35. package/dist/HoverCard/useHoverCard.js +6 -1
  36. package/dist/Overlay/Overlay.d.ts.map +1 -1
  37. package/dist/Overlay/Overlay.js +7 -2
  38. package/dist/PowerSearch/PowerSearch.d.ts +4 -2
  39. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  40. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +24 -1
  41. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +1 -1
  42. package/dist/PowerSearch/PowerSearchBottomSheet.js +172 -50
  43. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  44. package/dist/Resizable/ResizeHandle.js +15 -15
  45. package/dist/Resizable/createResizable.d.ts +70 -16
  46. package/dist/Resizable/createResizable.d.ts.map +1 -1
  47. package/dist/Resizable/createResizable.js +180 -22
  48. package/dist/Resizable/separatorAttributes.d.ts +45 -0
  49. package/dist/Resizable/separatorAttributes.d.ts.map +1 -0
  50. package/dist/Resizable/separatorAttributes.js +35 -0
  51. package/dist/Resizable/useResizable.js +2 -2
  52. package/dist/TimeInput/TimeWheels.d.ts +32 -0
  53. package/dist/TimeInput/TimeWheels.d.ts.map +1 -0
  54. package/dist/TimeInput/TimeWheels.js +144 -0
  55. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  56. package/dist/TimeInput/TouchTimeField.js +63 -118
  57. package/dist/TimeInput/timeWheelRules.d.ts +124 -0
  58. package/dist/TimeInput/timeWheelRules.d.ts.map +1 -0
  59. package/dist/TimeInput/timeWheelRules.js +277 -0
  60. package/dist/Tokenizer/TokenizerBottomSheet.js +8 -12
  61. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  62. package/dist/Tooltip/useTooltip.js +6 -0
  63. package/dist/astryx.css +3 -1
  64. package/dist/gestures/GestureHost.d.ts.map +1 -1
  65. package/dist/gestures/GestureHost.js +3 -0
  66. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +1 -1
  67. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +5 -2
  68. package/dist/i18n/generated-english/Resizable/createResizable.generated.d.ts +8 -0
  69. package/dist/i18n/generated-english/Resizable/createResizable.generated.d.ts.map +1 -0
  70. package/dist/i18n/generated-english/Resizable/createResizable.generated.js +13 -0
  71. package/dist/i18n/generated-english/TimeInput/TimeWheels.generated.d.ts +8 -0
  72. package/dist/i18n/generated-english/TimeInput/TimeWheels.generated.d.ts.map +1 -0
  73. package/dist/i18n/generated-english/TimeInput/TimeWheels.generated.js +17 -0
  74. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  75. package/dist/i18n/generated-locales/en.generated.js +5 -2
  76. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  77. package/dist/i18n/generated-locales/pseudo.generated.js +5 -2
  78. package/dist/keyboard/index.d.ts +17 -0
  79. package/dist/keyboard/index.d.ts.map +1 -0
  80. package/dist/keyboard/index.js +19 -0
  81. package/dist/keyboard/keyboardHeight.d.ts +149 -0
  82. package/dist/keyboard/keyboardHeight.d.ts.map +1 -0
  83. package/dist/keyboard/keyboardHeight.js +432 -0
  84. package/dist/keyboard/keyboardLayoutGuide.d.ts +59 -0
  85. package/dist/keyboard/keyboardLayoutGuide.d.ts.map +1 -0
  86. package/dist/keyboard/keyboardLayoutGuide.js +207 -0
  87. package/dist/keyboard/keyboardModel.d.ts +93 -0
  88. package/dist/keyboard/keyboardModel.d.ts.map +1 -0
  89. package/dist/keyboard/keyboardModel.js +220 -0
  90. package/dist/keyboard/rideKeyboard.d.ts +42 -0
  91. package/dist/keyboard/rideKeyboard.d.ts.map +1 -0
  92. package/dist/keyboard/rideKeyboard.js +77 -0
  93. package/dist/keyboard/useMobileKeyboard.d.ts +61 -0
  94. package/dist/keyboard/useMobileKeyboard.d.ts.map +1 -0
  95. package/dist/keyboard/useMobileKeyboard.js +699 -0
  96. package/dist/motion/android/index.d.ts +16 -0
  97. package/dist/motion/android/index.d.ts.map +1 -0
  98. package/dist/motion/android/index.js +18 -0
  99. package/dist/motion/android/keyboard.d.ts +83 -0
  100. package/dist/motion/android/keyboard.d.ts.map +1 -0
  101. package/dist/motion/android/keyboard.js +96 -0
  102. package/dist/motion/uikit/datePicker.d.ts +58 -0
  103. package/dist/motion/uikit/datePicker.d.ts.map +1 -0
  104. package/dist/motion/uikit/datePicker.js +60 -0
  105. package/dist/motion/uikit/index.d.ts +4 -3
  106. package/dist/motion/uikit/index.d.ts.map +1 -1
  107. package/dist/motion/uikit/index.js +4 -3
  108. package/dist/utils/interactionOverlay.stylex.d.ts +3 -2
  109. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  110. package/dist/utils/interactionOverlay.stylex.js +3 -2
  111. package/dist/utils/layoutViewport.d.ts +25 -0
  112. package/dist/utils/layoutViewport.d.ts.map +1 -0
  113. package/dist/utils/layoutViewport.js +102 -0
  114. package/dist/utils/pressFeedback.d.ts.map +1 -1
  115. package/dist/utils/pressFeedback.js +74 -9
  116. package/dist/utils/pressGesture.d.ts +4 -2
  117. package/dist/utils/pressGesture.d.ts.map +1 -1
  118. package/dist/utils/pressGesture.js +3 -1
  119. package/dist/utils/selectionLock.d.ts +10 -1
  120. package/dist/utils/selectionLock.d.ts.map +1 -1
  121. package/dist/utils/selectionLock.js +31 -5
  122. package/dist/utils/sharedMutationObserver.d.ts +2 -1
  123. package/dist/utils/sharedMutationObserver.d.ts.map +1 -1
  124. package/dist/utils/sharedMutationObserver.js +2 -1
  125. package/dist/utils/timeParser.d.ts +2 -1
  126. package/dist/utils/timeParser.d.ts.map +1 -1
  127. package/dist/utils/timeParser.js +29 -3
  128. package/locales/en.json +21 -9
  129. package/locales/pseudo.json +15 -6
  130. package/package.json +5 -5
  131. package/src/BottomSheet/BottomSheet.doc.mjs +1 -1
  132. package/src/BottomSheet/BottomSheetPanel.tsx +41 -13
  133. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +5 -0
  134. package/src/BottomSheet/bottomSheetStack.ts +1 -1
  135. package/src/BottomSheet/useSheetGestures.ts +6 -0
  136. package/src/CheckboxList/CheckboxList.doc.mjs +7 -0
  137. package/src/CheckboxList/CheckboxList.tsx +34 -2
  138. package/src/CheckboxList/CheckboxListContext.tsx +9 -1
  139. package/src/CheckboxList/CheckboxListItem.tsx +23 -2
  140. package/src/CheckboxList/requiredGroup.ts +77 -0
  141. package/src/DateInput/PickerWheel.tsx +37 -4
  142. package/src/DateInput/Wheel.tsx +30 -3
  143. package/src/DateInput/datePickerTouch.ts +15 -0
  144. package/src/DateInput/wheelOptions.ts +31 -2
  145. package/src/DateTimeInput/TouchDateTimeField.tsx +94 -178
  146. package/src/HoverCard/useHoverCard.tsx +6 -1
  147. package/src/Overlay/Overlay.tsx +7 -2
  148. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  149. package/src/PowerSearch/PowerSearch.tsx +4 -2
  150. package/src/PowerSearch/PowerSearchBottomSheet.tsx +237 -62
  151. package/src/Resizable/ResizeHandle.tsx +18 -26
  152. package/src/Resizable/createResizable.ts +242 -27
  153. package/src/Resizable/separatorAttributes.ts +73 -0
  154. package/src/Resizable/useResizable.ts +2 -2
  155. package/src/TimeInput/TimeWheels.tsx +218 -0
  156. package/src/TimeInput/TouchTimeField.tsx +93 -157
  157. package/src/TimeInput/timeWheelRules.ts +352 -0
  158. package/src/Tokenizer/TokenizerBottomSheet.tsx +8 -12
  159. package/src/Tooltip/useTooltip.tsx +6 -0
  160. package/src/gestures/GestureHost.ts +5 -4
  161. package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +5 -2
  162. package/src/i18n/generated-english/Resizable/createResizable.generated.ts +14 -0
  163. package/src/i18n/generated-english/TimeInput/TimeWheels.generated.ts +18 -0
  164. package/src/i18n/generated-locales/en.generated.ts +6 -4
  165. package/src/i18n/generated-locales/pseudo.generated.ts +6 -4
  166. package/src/keyboard/README.md +197 -0
  167. package/src/keyboard/index.ts +48 -0
  168. package/src/keyboard/keyboardHeight.ts +543 -0
  169. package/src/keyboard/keyboardLayoutGuide.ts +266 -0
  170. package/src/keyboard/keyboardModel.ts +301 -0
  171. package/src/keyboard/rideKeyboard.ts +100 -0
  172. package/src/keyboard/useMobileKeyboard.ts +866 -0
  173. package/src/motion/MEASUREMENTS.md +10 -0
  174. package/src/motion/README.md +2 -0
  175. package/src/motion/android/index.ts +18 -0
  176. package/src/motion/android/keyboard.ts +96 -0
  177. package/src/motion/uikit/datePicker.ts +60 -0
  178. package/src/motion/uikit/index.ts +4 -3
  179. package/src/utils/interactionOverlay.stylex.ts +3 -2
  180. package/src/utils/layoutViewport.ts +104 -0
  181. package/src/utils/pressFeedback.ts +99 -21
  182. package/src/utils/pressGesture.ts +4 -2
  183. package/src/utils/selectionLock.ts +40 -6
  184. package/src/utils/sharedMutationObserver.ts +2 -1
  185. package/src/utils/timeParser.ts +40 -3
  186. package/dist/BottomSheet/useMobileKeyboard.d.ts +0 -21
  187. package/dist/BottomSheet/useMobileKeyboard.d.ts.map +0 -1
  188. package/dist/BottomSheet/useMobileKeyboard.js +0 -413
  189. package/src/BottomSheet/useMobileKeyboard.ts +0 -542
@@ -0,0 +1,218 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file TimeWheels.tsx
7
+ * @input A time of day, the hour format, the bounds and a commit callback
8
+ * @output Exports TimeWheels: UIDatePicker's `.time` wheels as one picker
9
+ * @position Internal component; the wheels under a finger in the TimeInput
10
+ * sheet and DateTimeInput's time panel. timeWheelRules.ts holds the rules.
11
+ *
12
+ * A column that moves by itself turns on the Wheel's own spin (`spinTo`). The
13
+ * spin starts before the commit, so the new value finds the column moving.
14
+ *
15
+ * SYNC: When modified, update TimeWheels.test.tsx, timeWheelRules.ts,
16
+ * TimeInput.doc.mjs and DateTimeInput.doc.mjs.
17
+ */
18
+
19
+ import {useMemo, useRef} from 'react';
20
+ import {Wheel, type WheelHandle} from '../DateInput/Wheel';
21
+ import type {ISOTimeString} from '../utils/timeParser';
22
+ import {
23
+ columnValue,
24
+ columnsToTurn,
25
+ displayedTime,
26
+ restingTime,
27
+ hourRows,
28
+ meridiemOf,
29
+ meridiemRows,
30
+ minuteRows,
31
+ secondRows,
32
+ turnColumn,
33
+ wheelBounds,
34
+ type TimeColumn,
35
+ type TimeParts,
36
+ type TimeRow,
37
+ } from './timeWheelRules';
38
+ import {
39
+ registerEnglishFallback,
40
+ useComponentTranslator,
41
+ } from '../i18n/useComponentTranslator';
42
+ import english from '../i18n/generated-english/TimeInput/TimeWheels.generated';
43
+
44
+ registerEnglishFallback(english);
45
+
46
+ export interface TimeWheelsProps {
47
+ /** The time the wheels show. */
48
+ time: TimeParts;
49
+ /** 12-hour (with an AM/PM column) or 24-hour time. */
50
+ hourFormat: '12h' | '24h';
51
+ /** Whether a seconds column shows. */
52
+ hasSeconds: boolean;
53
+ /** Earliest time that may commit. */
54
+ min?: ISOTimeString;
55
+ /** Latest time that may commit. */
56
+ max?: ISOTimeString;
57
+ /** False while the wheels are hidden. */
58
+ isActive: boolean;
59
+ /** Called with the committed time, already inside the bounds. */
60
+ onCommit: (time: TimeParts) => void;
61
+ }
62
+
63
+ function sameTime(a: TimeParts, b: TimeParts): boolean {
64
+ return a.hour === b.hour && a.minute === b.minute && a.second === b.second;
65
+ }
66
+
67
+ /**
68
+ * UIDatePicker's time wheels.
69
+ *
70
+ * @example
71
+ * ```
72
+ * <TimeWheels time={{hour: 11, minute: 58, second: 0}} hourFormat="12h"
73
+ * hasSeconds={false} isActive onCommit={setTime} />
74
+ * ```
75
+ */
76
+ export function TimeWheels({
77
+ time,
78
+ hourFormat,
79
+ hasSeconds,
80
+ min,
81
+ max,
82
+ isActive,
83
+ onCommit,
84
+ }: TimeWheelsProps) {
85
+ const t = useComponentTranslator();
86
+ const bounds = useMemo(() => wheelBounds(min, max), [min, max]);
87
+ const shown = displayedTime(time, hasSeconds);
88
+
89
+ const hourOptions = useMemo(
90
+ () => hourRows(shown.hour, hourFormat, bounds),
91
+ [shown.hour, hourFormat, bounds],
92
+ );
93
+ const minuteOptions = useMemo(
94
+ () => minuteRows(shown.hour, hasSeconds, bounds),
95
+ [shown.hour, hasSeconds, bounds],
96
+ );
97
+ const secondOptions = useMemo(
98
+ () => secondRows(shown.hour, shown.minute, bounds),
99
+ [shown.hour, shown.minute, bounds],
100
+ );
101
+ const meridiemOptions = useMemo(
102
+ () =>
103
+ meridiemRows(
104
+ {
105
+ am: t('@astryx.dateTimeInput.meridiemAM'),
106
+ pm: t('@astryx.dateTimeInput.meridiemPM'),
107
+ },
108
+ bounds,
109
+ ),
110
+ [t, bounds],
111
+ );
112
+
113
+ const hourRef = useRef<WheelHandle | null>(null);
114
+ const minuteRef = useRef<WheelHandle | null>(null);
115
+ const secondRef = useRef<WheelHandle | null>(null);
116
+ const meridiemRef = useRef<WheelHandle | null>(null);
117
+
118
+ const rows: Record<TimeColumn, TimeRow[]> = {
119
+ hour: hourOptions,
120
+ minute: minuteOptions,
121
+ second: secondOptions,
122
+ meridiem: meridiemOptions,
123
+ };
124
+ // The rows the columns turn onto by themselves (`spinTo`), each with the id of
125
+ // its turn. A turn's arrival is not a person's rest, so it commits nothing,
126
+ // even when the owner kept the old time.
127
+ const spinsRef = useRef<
128
+ Partial<Record<TimeColumn, {value: number; id: number}>>
129
+ >({});
130
+ const nextSpinIdRef = useRef(0);
131
+
132
+ const restOn = (column: TimeColumn) => (value: number) => {
133
+ if (spinsRef.current[column]?.value === value) {
134
+ return;
135
+ }
136
+ const from = shown;
137
+ // What commits is the row the wheels come to rest on: past a bound, the
138
+ // nearest time inside it that has a row (datePicker.wheels.restPastBound).
139
+ const next = restingTime(
140
+ turnColumn(from, column, value, hourFormat),
141
+ hasSeconds,
142
+ bounds,
143
+ );
144
+ const handles: Record<TimeColumn, WheelHandle | null> = {
145
+ hour: hourRef.current,
146
+ minute: minuteRef.current,
147
+ second: secondRef.current,
148
+ meridiem: meridiemRef.current,
149
+ };
150
+ // Turn the other columns first, so the commit's new value finds them
151
+ // already moving (a Wheel never parks a moving column).
152
+ for (const turn of columnsToTurn(from, column, value, next, {
153
+ hourFormat,
154
+ hasSeconds,
155
+ })) {
156
+ const index = rows[turn.column].findIndex(
157
+ row => row.value === turn.value,
158
+ );
159
+ const handle = handles[turn.column];
160
+ if (index >= 0 && handle != null) {
161
+ nextSpinIdRef.current += 1;
162
+ const id = nextSpinIdRef.current;
163
+ spinsRef.current[turn.column] = {value: turn.value, id};
164
+ void handle.spinTo(index).finally(() => {
165
+ if (spinsRef.current[turn.column]?.id === id) {
166
+ spinsRef.current[turn.column] = undefined;
167
+ }
168
+ });
169
+ }
170
+ }
171
+ if (!sameTime(next, from)) {
172
+ onCommit(next);
173
+ }
174
+ };
175
+
176
+ return (
177
+ <>
178
+ <Wheel
179
+ ref={hourRef}
180
+ label={t('@astryx.dateTimeInput.hourWheel')}
181
+ options={hourOptions}
182
+ value={columnValue(shown, 'hour', hourFormat)}
183
+ isActive={isActive}
184
+ onChange={restOn('hour')}
185
+ />
186
+ <Wheel
187
+ ref={minuteRef}
188
+ label={t('@astryx.dateTimeInput.minuteWheel')}
189
+ options={minuteOptions}
190
+ value={shown.minute}
191
+ isActive={isActive}
192
+ onChange={restOn('minute')}
193
+ />
194
+ {hasSeconds && (
195
+ <Wheel
196
+ ref={secondRef}
197
+ label={t('@astryx.dateTimeInput.secondWheel')}
198
+ options={secondOptions}
199
+ value={shown.second}
200
+ isActive={isActive}
201
+ onChange={restOn('second')}
202
+ />
203
+ )}
204
+ {hourFormat === '12h' && (
205
+ <Wheel
206
+ ref={meridiemRef}
207
+ label={t('@astryx.dateTimeInput.meridiemWheel')}
208
+ options={meridiemOptions}
209
+ value={meridiemOf(shown.hour)}
210
+ isActive={isActive}
211
+ onChange={restOn('meridiem')}
212
+ />
213
+ )}
214
+ </>
215
+ );
216
+ }
217
+
218
+ TimeWheels.displayName = 'TimeWheels';
@@ -4,11 +4,11 @@
4
4
 
5
5
  /**
6
6
  * @file TouchTimeField.tsx
7
- * @input Uses React, Field, BottomSheet, Wheel, DateInput touch geometry
8
- * tokens, and shared time utilities
7
+ * @input Uses React, Field, BottomSheet, the wheels (TimeWheels under a finger,
8
+ * Wheel with a mouse), DateInput touch geometry tokens and time utilities
9
9
  * @output Exports TouchTimeField — the bottom-sheet surface behind TimeInput
10
10
  * @position Internal component; consumed by TimeInput.tsx for `presentation`
11
- * sheet surfaces (`spec:AST-043` DEC-3: the time half of DateTimeInput's sheet)
11
+ * sheet surfaces
12
12
  *
13
13
  * SYNC: When modified, update these files to stay in sync:
14
14
  * - /packages/core/src/TimeInput/TimeInput.tsx
@@ -54,6 +54,7 @@ import {useSize} from '../SizeContext/SizeContext';
54
54
  import {Spinner} from '../Spinner';
55
55
  import {useTooltip} from '../Tooltip';
56
56
  import {Wheel, type WheelOption} from '../DateInput/Wheel';
57
+ import {useTouchPresentation} from '../utils/touchPresentation';
57
58
  import {dateInputTouchGeometry} from '../DateInput/tokens.stylex';
58
59
  import {focusOutlineStyles} from '../utils/focusOutline.stylex';
59
60
  import {themeProps} from '../utils/themeProps';
@@ -68,6 +69,16 @@ import {
68
69
  } from '../utils';
69
70
  import {chainEventHandlers} from '../utils/chainEventHandlers';
70
71
  import type {TimeInputProps} from './TimeInput';
72
+ import {TimeWheels} from '../DateInput/datePickerTouch';
73
+ import {
74
+ hour12From24,
75
+ hour24From12,
76
+ hourRowsInHalf,
77
+ meridiemRows,
78
+ minuteRows,
79
+ secondRows,
80
+ wheelBounds,
81
+ } from './timeWheelRules';
71
82
  import {
72
83
  registerEnglishFallback,
73
84
  useComponentTranslator,
@@ -141,45 +152,6 @@ const styles = stylex.create({
141
152
  },
142
153
  });
143
154
 
144
- function twoDigits(value: number): string {
145
- return String(value).padStart(2, '0');
146
- }
147
-
148
- function hour12From24(hour: number): number {
149
- return hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
150
- }
151
-
152
- function hour24From12(hour: number, meridiem: number): number {
153
- if (meridiem === 0) {
154
- return hour === 12 ? 0 : hour;
155
- }
156
- return hour === 12 ? 12 : hour + 12;
157
- }
158
-
159
- function timeToSeconds(time: ISOTimeString | undefined): number | null {
160
- if (time == null) {
161
- return null;
162
- }
163
- const parsed = parseISOTime(time);
164
- return parsed == null
165
- ? null
166
- : parsed.hour * 3600 + parsed.minute * 60 + parsed.second;
167
- }
168
-
169
- function rangeOverlaps(
170
- startSecond: number,
171
- endSecond: number,
172
- min: ISOTimeString | undefined,
173
- max: ISOTimeString | undefined,
174
- ): boolean {
175
- const minSecond = timeToSeconds(min);
176
- const maxSecond = timeToSeconds(max);
177
- return (
178
- (minSecond == null || endSecond >= minSecond) &&
179
- (maxSecond == null || startSecond <= maxSecond)
180
- );
181
- }
182
-
183
155
  /**
184
156
  * The Astryx bottom-sheet time surface: a read-only closed field that opens
185
157
  * hour/minute(/second/meridiem) wheels in a BottomSheet. Wheels commit live,
@@ -249,6 +221,7 @@ export function TouchTimeField({
249
221
  const mergedInputRef = useMergedRefs(ref, inputRef);
250
222
  const inputGroup = useInputGroup();
251
223
  const [isSheetOpen, setIsSheetOpen] = useState(false);
224
+ const {isTouch} = useTouchPresentation();
252
225
  const [, startTransition] = useTransition();
253
226
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
254
227
  const isBusy = isLoading || optimisticValue !== value;
@@ -363,86 +336,29 @@ export function TouchTimeField({
363
336
  [fireChange, hasSeconds, max, min, optimisticValue],
364
337
  );
365
338
 
366
- const hourOptions: WheelOption[] = useMemo(() => {
367
- if (hourFormat === '24h') {
368
- return Array.from({length: 24}, (_, hour) => ({
369
- value: hour,
370
- label: twoDigits(hour),
371
- isDisabled: !rangeOverlaps(hour * 3600, hour * 3600 + 3599, min, max),
372
- }));
373
- }
374
- const meridiem = parsedWheelTime.hour < 12 ? 0 : 1;
375
- return Array.from({length: 12}, (_, index) => {
376
- const hour = index + 1;
377
- const hour24 = hour24From12(hour, meridiem);
378
- return {
379
- value: hour,
380
- label: String(hour),
381
- // Keys step in time order: 12 PM comes before 1 PM.
382
- order: hour24,
383
- isDisabled: !rangeOverlaps(
384
- hour24 * 3600,
385
- hour24 * 3600 + 3599,
386
- min,
387
- max,
388
- ),
389
- };
390
- });
391
- }, [hourFormat, max, min, parsedWheelTime.hour]);
392
-
339
+ const bounds = useMemo(() => wheelBounds(min, max), [min, max]);
340
+ const hourOptions: WheelOption[] = useMemo(
341
+ () => hourRowsInHalf(parsedWheelTime.hour, hourFormat, bounds),
342
+ [bounds, hourFormat, parsedWheelTime.hour],
343
+ );
393
344
  const minuteOptions: WheelOption[] = useMemo(
394
- () =>
395
- Array.from({length: 60}, (_, minute) => {
396
- const start = parsedWheelTime.hour * 3600 + minute * 60;
397
- return {
398
- value: minute,
399
- label: twoDigits(minute),
400
- isDisabled: !rangeOverlaps(
401
- start,
402
- start + (hasSeconds ? 59 : 0),
403
- min,
404
- max,
405
- ),
406
- };
407
- }),
408
- [hasSeconds, max, min, parsedWheelTime.hour],
345
+ () => minuteRows(parsedWheelTime.hour, hasSeconds, bounds),
346
+ [bounds, hasSeconds, parsedWheelTime.hour],
409
347
  );
410
-
411
348
  const secondOptions: WheelOption[] = useMemo(
412
- () =>
413
- Array.from({length: 60}, (_, second) => ({
414
- value: second,
415
- label: twoDigits(second),
416
- isDisabled: !isTimeInRange(
417
- formatISOTime(
418
- {
419
- hour: parsedWheelTime.hour,
420
- minute: parsedWheelTime.minute,
421
- second,
422
- },
423
- true,
424
- ),
425
- min,
426
- max,
427
- ),
428
- })),
429
- [max, min, parsedWheelTime.hour, parsedWheelTime.minute],
349
+ () => secondRows(parsedWheelTime.hour, parsedWheelTime.minute, bounds),
350
+ [bounds, parsedWheelTime.hour, parsedWheelTime.minute],
430
351
  );
431
-
432
352
  const meridiemOptions: WheelOption[] = useMemo(
433
- () => [
434
- {
435
- value: 0,
436
- label: t('@astryx.dateTimeInput.meridiemAM'),
437
- isDisabled: !rangeOverlaps(0, 11 * 3600 + 3599, min, max),
438
- },
439
- {
440
- value: 1,
441
- label: t('@astryx.dateTimeInput.meridiemPM'),
442
- isDisabled: !rangeOverlaps(12 * 3600, 23 * 3600 + 3599, min, max),
443
- },
444
- ],
445
- [max, min, t],
353
+ () =>
354
+ meridiemRows(
355
+ {
356
+ am: t('@astryx.dateTimeInput.meridiemAM'),
357
+ pm: t('@astryx.dateTimeInput.meridiemPM'),
358
+ },
359
+ bounds,
360
+ ),
361
+ [bounds, t],
446
362
  );
447
363
 
448
364
  const openSheet = useCallback(() => {
@@ -541,49 +457,69 @@ export function TouchTimeField({
541
457
  padding={0}>
542
458
  <div ref={sheetBodyRef} {...stylex.props(styles.sheetBody)}>
543
459
  <div {...stylex.props(styles.wheels)}>
544
- <Wheel
545
- label={t('@astryx.dateTimeInput.hourWheel')}
546
- options={hourOptions}
547
- value={
548
- hourFormat === '24h'
549
- ? parsedWheelTime.hour
550
- : hour12From24(parsedWheelTime.hour)
551
- }
552
- isActive={isSheetOpen}
553
- onChange={hour =>
554
- commitWheelTime(
555
- hourFormat === '24h'
556
- ? composeWheelTime({hour24: hour})
557
- : composeWheelTime({hour12: hour}),
558
- )
559
- }
560
- />
561
- <Wheel
562
- label={t('@astryx.dateTimeInput.minuteWheel')}
563
- options={minuteOptions}
564
- value={parsedWheelTime.minute}
565
- isActive={isSheetOpen}
566
- onChange={minute => commitWheelTime(composeWheelTime({minute}))}
567
- />
568
- {hasSeconds && (
569
- <Wheel
570
- label={t('@astryx.dateTimeInput.secondWheel')}
571
- options={secondOptions}
572
- value={parsedWheelTime.second}
573
- isActive={isSheetOpen}
574
- onChange={second => commitWheelTime(composeWheelTime({second}))}
575
- />
576
- )}
577
- {hourFormat === '12h' && (
578
- <Wheel
579
- label={t('@astryx.dateTimeInput.meridiemWheel')}
580
- options={meridiemOptions}
581
- value={parsedWheelTime.hour < 12 ? 0 : 1}
460
+ {isTouch ? (
461
+ <TimeWheels
462
+ time={parsedWheelTime}
463
+ hourFormat={hourFormat}
464
+ hasSeconds={hasSeconds}
465
+ min={min}
466
+ max={max}
582
467
  isActive={isSheetOpen}
583
- onChange={meridiem =>
584
- commitWheelTime(composeWheelTime({meridiem}))
468
+ onCommit={time =>
469
+ commitWheelTime(formatISOTime(time, hasSeconds))
585
470
  }
586
471
  />
472
+ ) : (
473
+ <>
474
+ <Wheel
475
+ label={t('@astryx.dateTimeInput.hourWheel')}
476
+ options={hourOptions}
477
+ value={
478
+ hourFormat === '24h'
479
+ ? parsedWheelTime.hour
480
+ : hour12From24(parsedWheelTime.hour)
481
+ }
482
+ isActive={isSheetOpen}
483
+ onChange={hour =>
484
+ commitWheelTime(
485
+ hourFormat === '24h'
486
+ ? composeWheelTime({hour24: hour})
487
+ : composeWheelTime({hour12: hour}),
488
+ )
489
+ }
490
+ />
491
+ <Wheel
492
+ label={t('@astryx.dateTimeInput.minuteWheel')}
493
+ options={minuteOptions}
494
+ value={parsedWheelTime.minute}
495
+ isActive={isSheetOpen}
496
+ onChange={minute =>
497
+ commitWheelTime(composeWheelTime({minute}))
498
+ }
499
+ />
500
+ {hasSeconds && (
501
+ <Wheel
502
+ label={t('@astryx.dateTimeInput.secondWheel')}
503
+ options={secondOptions}
504
+ value={parsedWheelTime.second}
505
+ isActive={isSheetOpen}
506
+ onChange={second =>
507
+ commitWheelTime(composeWheelTime({second}))
508
+ }
509
+ />
510
+ )}
511
+ {hourFormat === '12h' && (
512
+ <Wheel
513
+ label={t('@astryx.dateTimeInput.meridiemWheel')}
514
+ options={meridiemOptions}
515
+ value={parsedWheelTime.hour < 12 ? 0 : 1}
516
+ isActive={isSheetOpen}
517
+ onChange={meridiem =>
518
+ commitWheelTime(composeWheelTime({meridiem}))
519
+ }
520
+ />
521
+ )}
522
+ </>
587
523
  )}
588
524
  </div>
589
525
  <div {...stylex.props(styles.footer)}>