@proyecto-viviana/solid-stately 0.4.0 → 0.4.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 (33) hide show
  1. package/dist/calendar/createDateFieldState.d.ts +56 -18
  2. package/dist/calendar/createRangeCalendarState.d.ts +10 -0
  3. package/dist/calendar/createTimeFieldState.d.ts +38 -67
  4. package/dist/calendar/intl/index.d.ts +10 -0
  5. package/dist/collections/ListCollection.d.ts +8 -0
  6. package/dist/collections/createListState.d.ts +12 -0
  7. package/dist/collections/getChildNodes.d.ts +8 -0
  8. package/dist/collections/index.d.ts +2 -2
  9. package/dist/collections/types.d.ts +9 -0
  10. package/dist/color/intl/index.d.ts +14 -0
  11. package/dist/dnd/createDroppableCollectionState.d.ts +17 -2
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +3444 -1304
  14. package/dist/index.js.map +1 -1
  15. package/dist/selection/SelectionManager.d.ts +9 -0
  16. package/package.json +1 -1
  17. package/src/calendar/createCalendarState.ts +26 -4
  18. package/src/calendar/createDateFieldState.ts +614 -459
  19. package/src/calendar/createRangeCalendarState.ts +212 -11
  20. package/src/calendar/createTimeFieldState.ts +179 -467
  21. package/src/calendar/intl/index.ts +364 -0
  22. package/src/collections/ListCollection.ts +31 -0
  23. package/src/collections/createListState.ts +92 -1
  24. package/src/collections/getChildNodes.ts +31 -0
  25. package/src/collections/index.ts +3 -0
  26. package/src/collections/types.ts +10 -0
  27. package/src/color/Color.ts +104 -133
  28. package/src/color/intl/index.ts +1409 -0
  29. package/src/combobox/createComboBoxState.ts +30 -13
  30. package/src/dnd/createDroppableCollectionState.ts +125 -6
  31. package/src/index.ts +3 -0
  32. package/src/selection/SelectionManager.ts +15 -0
  33. package/src/steplist/createStepListState.ts +27 -4
@@ -1,53 +1,57 @@
1
1
  /**
2
2
  * TimeFieldState for Solid-Stately
3
3
  *
4
- * Provides state management for time field components with segment-based editing.
5
- * Based on @react-stately/datepicker useTimeFieldState
4
+ * Faithful port of `@react-stately/datepicker` `useTimeFieldState`, which is a
5
+ * thin wrapper over `useDateFieldState`: a time-only `Time` value is converted
6
+ * to a `CalendarDateTime` (anchored on today's date, or a zoned date when the
7
+ * value/default carries a time zone) and fed to the date-field state machine
8
+ * with `maxGranularity: 'hour'` so the date segments drop away and only
9
+ * hour/minute[/second]/dayPeriod remain. The returned state IS the
10
+ * `DateFieldState`, plus a `timeValue` accessor holding the value back in the
11
+ * `Time` domain. This means TimeField reuses the certified DateField segment
12
+ * stack (`createDateSegment`, `DateInput`, `DateSegment`) exactly as RAC's
13
+ * TimeField reuses `DateInput`/`DateSegment` — there is no parallel time
14
+ * primitive.
15
+ *
16
+ * The `timeValue` is layered on with a Proxy rather than an object spread
17
+ * because `DateFieldState`'s `calendar`/`dateFormatter`/`granularity`/
18
+ * `maxGranularity`/`locale`/`timeZone` members are getters — spreading would
19
+ * invoke and freeze them, breaking reactivity.
6
20
  */
7
21
 
8
22
  import { createSignal, createMemo, type Accessor } from "solid-js";
9
23
  import {
10
- type Time,
24
+ Time,
25
+ type CalendarDate,
11
26
  type CalendarDateTime,
12
27
  type ZonedDateTime,
13
28
  type DateValue,
14
29
  getLocalTimeZone,
15
- DateFormatter,
30
+ today,
31
+ toTime,
32
+ toZoned,
33
+ toCalendarDateTime,
34
+ GregorianCalendar,
16
35
  } from "@internationalized/date";
17
36
  import { access, type MaybeAccessor } from "../utils";
18
37
  import type { ValidationState } from "./createCalendarState";
38
+ import type { ValidationBehavior, ValidationFunction } from "../form";
19
39
  import {
20
- createFormValidationState,
21
- DEFAULT_VALIDATION_RESULT,
22
- VALID_VALIDITY_STATE,
23
- type ValidationBehavior,
24
- type ValidationFunction,
25
- type ValidationResult,
26
- type ValidityState,
27
- } from "../form";
40
+ createDateFieldState,
41
+ type DateFieldState,
42
+ type DateSegment as DateSegmentInternal,
43
+ type DateSegmentType as DateSegmentTypeInternal,
44
+ } from "./createDateFieldState";
28
45
 
29
46
  export type TimeValue = Time | CalendarDateTime | ZonedDateTime;
30
47
 
31
- export type TimeSegmentType = "hour" | "minute" | "second" | "dayPeriod" | "literal";
32
-
33
- export interface TimeSegment {
34
- /** The type of segment. */
35
- type: TimeSegmentType;
36
- /** The text content of the segment. */
37
- text: string;
38
- /** The numeric value of the segment (if applicable). */
39
- value?: number;
40
- /** The minimum value for the segment. */
41
- minValue?: number;
42
- /** The maximum value for the segment. */
43
- maxValue?: number;
44
- /** Whether this segment is editable. */
45
- isEditable: boolean;
46
- /** Whether this segment is a placeholder. */
47
- isPlaceholder: boolean;
48
- /** A placeholder string for the segment. */
49
- placeholder: string;
50
- }
48
+ /**
49
+ * TimeField reuses the DateField segment vocabulary (RAC TimeField reuses
50
+ * `DateInput`/`DateSegment`); the legacy `TimeSegment`/`TimeSegmentType` names
51
+ * are kept as aliases of the shared date-segment types.
52
+ */
53
+ export type TimeSegmentType = DateSegmentTypeInternal;
54
+ export type TimeSegment = DateSegmentInternal;
51
55
 
52
56
  export interface TimeFieldStateProps<T extends TimeValue = Time> {
53
57
  /** The current value (controlled). */
@@ -74,6 +78,8 @@ export interface TimeFieldStateProps<T extends TimeValue = Time> {
74
78
  granularity?: "hour" | "minute" | "second";
75
79
  /** Whether to show 12 or 24 hour format. */
76
80
  hourCycle?: 12 | 24;
81
+ /** Whether to hide the time zone. */
82
+ hideTimeZone?: boolean;
77
83
  /** Whether to force leading zeroes in the hour segment. */
78
84
  shouldForceLeadingZeros?: boolean;
79
85
  /** Validation state. */
@@ -81,7 +87,7 @@ export interface TimeFieldStateProps<T extends TimeValue = Time> {
81
87
  /** Validation behavior mode. */
82
88
  validationBehavior?: ValidationBehavior;
83
89
  /** Custom validation function. */
84
- validate?: ValidationFunction<T>;
90
+ validate?: ValidationFunction<T | null>;
85
91
  /** Description text. */
86
92
  description?: string;
87
93
  /** Error message. */
@@ -92,49 +98,34 @@ export interface TimeFieldStateProps<T extends TimeValue = Time> {
92
98
  isDateUnavailable?: (date: DateValue) => boolean;
93
99
  }
94
100
 
95
- export interface TimeFieldState<T extends TimeValue = Time> {
96
- /** The current value. */
97
- value: Accessor<T | null>;
98
- /** The default value. */
99
- defaultValue: T | null;
100
- /** Sets the value. */
101
- setValue: (value: T | null) => void;
102
- /** The segments that make up the time. */
103
- segments: Accessor<TimeSegment[]>;
104
- /** Sets a segment value. */
105
- setSegment: (type: TimeSegmentType, value: number) => void;
106
- /** Increments a segment. */
107
- incrementSegment: (type: TimeSegmentType) => void;
108
- /** Decrements a segment. */
109
- decrementSegment: (type: TimeSegmentType) => void;
110
- /** Clears a segment. */
111
- clearSegment: (type: TimeSegmentType) => void;
112
- /** Whether the field is disabled. */
113
- isDisabled: Accessor<boolean>;
114
- /** Whether the field is read-only. */
115
- isReadOnly: Accessor<boolean>;
116
- /** Whether the field is required. */
117
- isRequired: Accessor<boolean>;
118
- /** The validation state. */
119
- validationState: Accessor<ValidationState | undefined>;
120
- /** The granularity. */
121
- granularity: "hour" | "minute" | "second";
122
- /** Whether the value is invalid. */
123
- isInvalid: Accessor<boolean>;
124
- /** Realtime validation results, including native and custom constraints. */
125
- realtimeValidation: Accessor<ValidationResult>;
126
- /** Currently displayed validation results. */
127
- displayValidation: Accessor<ValidationResult>;
128
- /** Updates the current validation result. */
129
- updateValidation: (result: ValidationResult) => void;
130
- /** Resets displayed validation to valid. */
131
- resetValidation: () => void;
132
- /** Commits realtime validation to displayed validation. */
133
- commitValidation: () => void;
134
- /** The locale. */
135
- locale: string;
136
- /** The hour cycle. */
137
- hourCycle: 12 | 24;
101
+ /**
102
+ * The TimeField state IS the certified DateField state (segments, validation,
103
+ * increment/decrement, formatter, etc.) with a `timeValue` accessor added. The
104
+ * `T` generic is retained for API back-compat with existing `TimeFieldState<T>`
105
+ * consumers.
106
+ */
107
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
108
+ export interface TimeFieldState<T extends TimeValue = Time> extends DateFieldState {
109
+ /** The current value in the `Time`/`CalendarDateTime`/`ZonedDateTime` domain. */
110
+ timeValue: Accessor<TimeValue | null>;
111
+ }
112
+
113
+ /**
114
+ * Anchors a `Time` onto a date so the date-field machine can edit it. Values
115
+ * that already carry a date (`CalendarDateTime`/`ZonedDateTime`) pass through.
116
+ * Mirrors `@react-stately/datepicker`'s `convertValue`.
117
+ */
118
+ function convertValue(
119
+ value: TimeValue | null | undefined,
120
+ date: DateValue = today(getLocalTimeZone()),
121
+ ): CalendarDateTime | ZonedDateTime | null {
122
+ if (!value) {
123
+ return null;
124
+ }
125
+ if ("day" in value) {
126
+ return value as CalendarDateTime | ZonedDateTime;
127
+ }
128
+ return toCalendarDateTime(date as CalendarDate | CalendarDateTime | ZonedDateTime, value as Time);
138
129
  }
139
130
 
140
131
  /**
@@ -143,420 +134,141 @@ export interface TimeFieldState<T extends TimeValue = Time> {
143
134
  export function createTimeFieldState<T extends TimeValue = Time>(
144
135
  props: TimeFieldStateProps<T> = {},
145
136
  ): TimeFieldState<T> {
146
- const timeZone = getLocalTimeZone();
147
- const locale = props.locale ?? "en-US";
148
- const granularity = props.granularity ?? "minute";
149
- const hourCycle = props.hourCycle ?? 12;
137
+ const placeholderValue: TimeValue = props.placeholderValue ?? new Time();
150
138
 
151
- const initialValue = props.defaultValue ?? null;
152
-
153
- // State signals
154
- const [internalValue, setInternalValue] = createSignal<T | null>(initialValue);
155
-
156
- // Track partial values during editing
157
- const [placeholderParts, setPlaceholderParts] = createSignal<Partial<TimeParts>>({});
158
-
159
- // Controlled vs uncontrolled value
160
- const value = createMemo<T | null>(() => {
139
+ // Controlled/uncontrolled Time value (mirrors useControlledState).
140
+ const [internalValue, setInternalValue] = createSignal<T | null>(props.defaultValue ?? null);
141
+ const value = (): T | null => {
161
142
  const controlled = access(props.value);
162
143
  return controlled !== undefined ? controlled : internalValue();
163
- });
144
+ };
164
145
 
165
- // Derived states
166
- const isDisabled = createMemo(() => access(props.isDisabled) ?? false);
167
- const isReadOnly = createMemo(() => access(props.isReadOnly) ?? false);
168
- const isRequired = createMemo(() => access(props.isRequired) ?? false);
169
- const explicitValidationState = createMemo(() => access(props.validationState));
170
- const validationBehavior = () => props.validationBehavior ?? "native";
171
- const builtinValidation = createMemo<ValidationResult>(() => {
172
- const v = value();
173
- if (!v) return DEFAULT_VALIDATION_RESULT;
146
+ const defaultValueTimeZone =
147
+ props.defaultValue && "timeZone" in props.defaultValue
148
+ ? (props.defaultValue as ZonedDateTime).timeZone
149
+ : undefined;
174
150
 
175
- const minValue = access(props.minValue);
176
- const maxValue = access(props.maxValue);
177
- const validationDetails: ValidityState = { ...VALID_VALIDITY_STATE };
178
- const validationErrors: string[] = [];
151
+ const v = (): TimeValue => value() || placeholderValue;
152
+ const day = (): DateValue | undefined => {
153
+ const current = v();
154
+ return current && "day" in current ? (current as CalendarDateTime | ZonedDateTime) : undefined;
155
+ };
179
156
 
180
- if (minValue && compareTime(v, minValue) < 0) {
181
- validationDetails.rangeUnderflow = true;
182
- validationErrors.push("Value is below the minimum time.");
157
+ const placeholderDate = createMemo<CalendarDateTime | ZonedDateTime | null>(() => {
158
+ const current = v();
159
+ const valueTimeZone =
160
+ current && "timeZone" in current ? (current as ZonedDateTime).timeZone : undefined;
161
+ const zone = valueTimeZone || defaultValueTimeZone;
162
+ if (zone && placeholderValue) {
163
+ return toZoned(convertValue(placeholderValue) as CalendarDateTime, zone);
183
164
  }
165
+ return convertValue(placeholderValue);
166
+ });
184
167
 
185
- if (maxValue && compareTime(v, maxValue) > 0) {
186
- validationDetails.rangeOverflow = true;
187
- validationErrors.push("Value is above the maximum time.");
188
- }
168
+ const minDate = createMemo(() => convertValue(access(props.minValue), day()) ?? undefined);
169
+ const maxDate = createMemo(() => convertValue(access(props.maxValue), day()) ?? undefined);
189
170
 
190
- if (validationErrors.length === 0) {
191
- return DEFAULT_VALIDATION_RESULT;
171
+ const timeValue = createMemo<TimeValue | null>(() => {
172
+ const current = value();
173
+ return current && "day" in current
174
+ ? toTime(current as CalendarDateTime | ZonedDateTime)
175
+ : current;
176
+ });
177
+ const dateTime = createMemo<DateValue | null>(() => {
178
+ const current = value();
179
+ return current == null ? null : convertValue(current);
180
+ });
181
+ const defaultDateTime = props.defaultValue == null ? null : convertValue(props.defaultValue);
182
+
183
+ // Route the inner date-field onChange back into the Time domain, mirroring
184
+ // upstream: keep the CalendarDateTime when the value carries a date/zone,
185
+ // otherwise collapse to a plain Time.
186
+ const onChange = (newValue: DateValue | null): void => {
187
+ const converted = (
188
+ day() || defaultValueTimeZone
189
+ ? newValue
190
+ : newValue && toTime(newValue as CalendarDateTime | ZonedDateTime)
191
+ ) as T | null;
192
+ if (access(props.value) === undefined) {
193
+ setInternalValue(() => converted);
192
194
  }
195
+ props.onChange?.(converted);
196
+ };
193
197
 
194
- validationDetails.valid = false;
195
- return {
196
- isInvalid: true,
197
- validationDetails,
198
- validationErrors,
199
- };
200
- });
201
- const validation = createFormValidationState<T>({
198
+ const dateState = createDateFieldState({
202
199
  get value() {
203
- return value();
200
+ return dateTime();
201
+ },
202
+ defaultValue: defaultDateTime,
203
+ onChange,
204
+ get minValue() {
205
+ return minDate();
206
+ },
207
+ get maxValue() {
208
+ return maxDate();
209
+ },
210
+ get isDisabled() {
211
+ return access(props.isDisabled);
212
+ },
213
+ get isReadOnly() {
214
+ return access(props.isReadOnly);
215
+ },
216
+ get isRequired() {
217
+ return access(props.isRequired);
204
218
  },
205
219
  get isInvalid() {
206
220
  return props.isInvalid;
207
221
  },
222
+ get locale() {
223
+ return props.locale;
224
+ },
225
+ get granularity() {
226
+ return props.granularity || "minute";
227
+ },
228
+ maxGranularity: "hour",
229
+ get hourCycle() {
230
+ return props.hourCycle;
231
+ },
232
+ get hideTimeZone() {
233
+ return props.hideTimeZone;
234
+ },
235
+ get placeholderValue() {
236
+ return placeholderDate() ?? undefined;
237
+ },
238
+ get shouldForceLeadingZeros() {
239
+ return props.shouldForceLeadingZeros;
240
+ },
241
+ createCalendar: () => new GregorianCalendar(),
208
242
  get validationState() {
209
- return explicitValidationState();
243
+ return access(props.validationState);
210
244
  },
211
245
  get validationBehavior() {
212
- return validationBehavior();
246
+ return props.validationBehavior;
247
+ },
248
+ validate: props.validate ? () => props.validate?.(value()) : undefined,
249
+ get description() {
250
+ return props.description;
213
251
  },
214
- get validate() {
215
- return props.validate;
252
+ get errorMessage() {
253
+ return props.errorMessage;
216
254
  },
217
- get builtinValidation() {
218
- return builtinValidation();
255
+ get isDateUnavailable() {
256
+ return props.isDateUnavailable;
219
257
  },
220
258
  });
221
- const isInvalid = createMemo(() => validation.displayValidation().isInvalid);
222
- const validationState = createMemo<ValidationState | undefined>(
223
- () => explicitValidationState() ?? (isInvalid() ? "invalid" : undefined),
224
- );
225
-
226
- // Generate segments based on granularity
227
- const segments = createMemo<TimeSegment[]>(() => {
228
- const v = value();
229
- const parts = placeholderParts();
230
- const segs: TimeSegment[] = [];
231
-
232
- // Determine format options
233
- const shouldForceLeadingZeros = props.shouldForceLeadingZeros === true;
234
- const formatOptions: Intl.DateTimeFormatOptions = {
235
- hour: shouldForceLeadingZeros ? "2-digit" : "numeric",
236
- minute: "2-digit",
237
- hourCycle: hourCycle === 12 ? "h12" : "h23",
238
- };
239
-
240
- if (granularity === "second") {
241
- formatOptions.second = "2-digit";
242
- }
243
-
244
- // Use a base date for formatting
245
- const baseDate = new Date();
246
- if (v) {
247
- baseDate.setHours(getHour(v), getMinute(v), getSecond(v));
248
- }
249
-
250
- const formatter = new DateFormatter(locale, formatOptions);
251
- const formattedParts = formatter.formatToParts(baseDate);
252
-
253
- for (const part of formattedParts) {
254
- const type = mapTimePartType(part.type);
255
259
 
256
- if (type === "literal") {
257
- segs.push({
258
- type: "literal",
259
- text: part.value,
260
- isEditable: false,
261
- isPlaceholder: false,
262
- placeholder: part.value,
263
- });
264
- } else if (type) {
265
- const segValue = getTimeSegmentValue(v, type);
266
- const partValue = parts[type as keyof TimeParts];
267
- const hasValue = v !== null || partValue !== undefined;
268
-
269
- segs.push({
270
- type,
271
- text: hasValue ? part.value : getTimePlaceholderText(type),
272
- value: segValue ?? partValue,
273
- minValue: getTimeMinValue(type, hourCycle),
274
- maxValue: getTimeMaxValue(type, hourCycle),
275
- isEditable: !isDisabled() && !isReadOnly(),
276
- isPlaceholder: !hasValue,
277
- placeholder: getTimePlaceholderText(type),
278
- });
260
+ // Add `timeValue` without spreading (spread would freeze DateFieldState's
261
+ // getter members). The same proxy identity is threaded to createTimeField and
262
+ // the field/segment contexts so `hookData` WeakMap keying stays consistent.
263
+ return new Proxy(dateState, {
264
+ get(target, prop, receiver) {
265
+ if (prop === "timeValue") {
266
+ return timeValue;
279
267
  }
280
- }
281
-
282
- return segs;
283
- });
284
-
285
- // Set value with onChange callback
286
- const setValue = (newValue: T | null) => {
287
- if (isDisabled() || isReadOnly()) return;
288
-
289
- const controlled = access(props.value);
290
- if (controlled === undefined) {
291
- setInternalValue(() => newValue);
292
- }
293
-
294
- if (props.onChange) {
295
- props.onChange(newValue);
296
- }
297
-
298
- setPlaceholderParts({});
299
- };
300
-
301
- // Set a specific segment value
302
- const setSegment = (type: TimeSegmentType, newValue: number) => {
303
- if (isDisabled() || isReadOnly()) return;
304
- if (type === "literal") return;
305
-
306
- const v = value();
307
-
308
- if (v) {
309
- const updated = updateTimePart(v, type, newValue, hourCycle);
310
- setValue(updated as T);
311
- } else {
312
- setPlaceholderParts((prev) => ({
313
- ...prev,
314
- [type]: newValue,
315
- }));
316
- }
317
- };
318
-
319
- // Increment a segment
320
- const incrementSegment = (type: TimeSegmentType) => {
321
- if (isDisabled() || isReadOnly()) return;
322
- if (type === "literal") return;
323
-
324
- const v = value();
325
- const current = v ? getTimeSegmentValue(v, type) : placeholderParts()[type as keyof TimeParts];
326
- const max = getTimeMaxValue(type, hourCycle);
327
- const min = getTimeMinValue(type, hourCycle);
328
-
329
- if (type === "dayPeriod") {
330
- // Toggle AM/PM
331
- const currentHour = v ? getHour(v) : (placeholderParts().hour ?? 0);
332
- const newHour = currentHour >= 12 ? currentHour - 12 : currentHour + 12;
333
- setSegment("hour", newHour);
334
- return;
335
- }
336
-
337
- const newValue = current !== undefined ? current + 1 : min;
338
- setSegment(type, newValue > max ? min : newValue);
339
- };
340
-
341
- // Decrement a segment
342
- const decrementSegment = (type: TimeSegmentType) => {
343
- if (isDisabled() || isReadOnly()) return;
344
- if (type === "literal") return;
345
-
346
- const v = value();
347
- const current = v ? getTimeSegmentValue(v, type) : placeholderParts()[type as keyof TimeParts];
348
- const max = getTimeMaxValue(type, hourCycle);
349
- const min = getTimeMinValue(type, hourCycle);
350
-
351
- if (type === "dayPeriod") {
352
- // Toggle AM/PM
353
- const currentHour = v ? getHour(v) : (placeholderParts().hour ?? 0);
354
- const newHour = currentHour >= 12 ? currentHour - 12 : currentHour + 12;
355
- setSegment("hour", newHour);
356
- return;
357
- }
358
-
359
- const newValue = current !== undefined ? current - 1 : max;
360
- setSegment(type, newValue < min ? max : newValue);
361
- };
362
-
363
- // Clear a segment
364
- const clearSegment = (type: TimeSegmentType) => {
365
- if (isDisabled() || isReadOnly()) return;
366
-
367
- const v = value();
368
- if (v) {
369
- setValue(null);
370
- } else {
371
- setPlaceholderParts((prev) => {
372
- const next = { ...prev };
373
- delete next[type as keyof TimeParts];
374
- return next;
375
- });
376
- }
377
- };
378
-
379
- return {
380
- value,
381
- defaultValue: initialValue,
382
- setValue,
383
- segments,
384
- setSegment,
385
- incrementSegment,
386
- decrementSegment,
387
- clearSegment,
388
- isDisabled,
389
- isReadOnly,
390
- isRequired,
391
- validationState,
392
- granularity,
393
- isInvalid,
394
- realtimeValidation: validation.realtimeValidation,
395
- displayValidation: validation.displayValidation,
396
- updateValidation: validation.updateValidation,
397
- resetValidation: validation.resetValidation,
398
- commitValidation: validation.commitValidation,
399
- locale,
400
- hourCycle,
401
- };
402
- }
403
-
404
- interface TimeParts {
405
- hour?: number;
406
- minute?: number;
407
- second?: number;
408
- }
409
-
410
- function mapTimePartType(type: Intl.DateTimeFormatPartTypes): TimeSegmentType | null {
411
- switch (type) {
412
- case "hour":
413
- return "hour";
414
- case "minute":
415
- return "minute";
416
- case "second":
417
- return "second";
418
- case "dayPeriod":
419
- return "dayPeriod";
420
- case "literal":
421
- return "literal";
422
- default:
423
- return null;
424
- }
425
- }
426
-
427
- function getHour(time: TimeValue): number {
428
- if ("hour" in time) {
429
- return time.hour;
430
- }
431
- return 0;
432
- }
433
-
434
- function getMinute(time: TimeValue): number {
435
- if ("minute" in time) {
436
- return time.minute;
437
- }
438
- return 0;
439
- }
440
-
441
- function getSecond(time: TimeValue): number {
442
- if ("second" in time) {
443
- return time.second;
444
- }
445
- return 0;
446
- }
447
-
448
- function getTimeSegmentValue(time: TimeValue | null, type: TimeSegmentType): number | undefined {
449
- if (!time) return undefined;
450
-
451
- switch (type) {
452
- case "hour":
453
- return getHour(time);
454
- case "minute":
455
- return getMinute(time);
456
- case "second":
457
- return getSecond(time);
458
- case "dayPeriod":
459
- return getHour(time) >= 12 ? 1 : 0;
460
- default:
461
- return undefined;
462
- }
463
- }
464
-
465
- function getTimeMinValue(type: TimeSegmentType, hourCycle: number): number {
466
- switch (type) {
467
- case "hour":
468
- return hourCycle === 12 ? 1 : 0;
469
- case "minute":
470
- return 0;
471
- case "second":
472
- return 0;
473
- case "dayPeriod":
474
- return 0;
475
- default:
476
- return 0;
477
- }
478
- }
479
-
480
- function getTimeMaxValue(type: TimeSegmentType, hourCycle: number): number {
481
- switch (type) {
482
- case "hour":
483
- // In 12-hour mode, the spinbutton displays 1–12, so aria-valuemax is 12.
484
- return hourCycle === 12 ? 12 : 23;
485
- case "minute":
486
- return 59;
487
- case "second":
488
- return 59;
489
- case "dayPeriod":
490
- return 1;
491
- default:
492
- return 0;
493
- }
494
- }
495
-
496
- function getTimePlaceholderText(type: TimeSegmentType): string {
497
- switch (type) {
498
- case "hour":
499
- return "––";
500
- case "minute":
501
- return "––";
502
- case "second":
503
- return "––";
504
- case "dayPeriod":
505
- return "AM";
506
- default:
507
- return "";
508
- }
509
- }
510
-
511
- function updateTimePart(
512
- time: TimeValue,
513
- type: TimeSegmentType,
514
- value: number,
515
- _hourCycle: number,
516
- ): TimeValue {
517
- if (!time) return time;
518
-
519
- // All TimeValue types in @internationalized/date have a .set() method
520
- const settable = time as { set(props: Record<string, number>): TimeValue };
521
- if (typeof settable.set !== "function") {
522
- return time;
523
- }
524
-
525
- switch (type) {
526
- case "hour":
527
- return settable.set({ hour: value });
528
- case "minute":
529
- return settable.set({ minute: value });
530
- case "second":
531
- return settable.set({ second: value });
532
- case "dayPeriod": {
533
- // dayPeriod 0 = AM, 1 = PM. Adjust hour by +/- 12 based on desired period.
534
- const currentHour = getHour(time);
535
- const isPM = value === 1;
536
- const isCurrentlyPM = currentHour >= 12;
537
- if (isPM && !isCurrentlyPM) {
538
- return settable.set({ hour: currentHour + 12 });
539
- }
540
- if (!isPM && isCurrentlyPM) {
541
- return settable.set({ hour: currentHour - 12 });
542
- }
543
- return time;
544
- }
545
- default:
546
- return time;
547
- }
548
- }
549
-
550
- function compareTime(a: TimeValue, b: TimeValue): number {
551
- const aHour = getHour(a);
552
- const bHour = getHour(b);
553
- if (aHour !== bHour) return aHour - bHour;
554
-
555
- const aMinute = getMinute(a);
556
- const bMinute = getMinute(b);
557
- if (aMinute !== bMinute) return aMinute - bMinute;
558
-
559
- const aSecond = getSecond(a);
560
- const bSecond = getSecond(b);
561
- return aSecond - bSecond;
268
+ return Reflect.get(target, prop, receiver);
269
+ },
270
+ has(target, prop) {
271
+ return prop === "timeValue" || Reflect.has(target, prop);
272
+ },
273
+ }) as unknown as TimeFieldState<T>;
562
274
  }