@proyecto-viviana/solid-stately 0.3.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.
- package/dist/calendar/IncompleteDate.d.ts +61 -0
- package/dist/calendar/createCalendarState.d.ts +20 -10
- package/dist/calendar/createDateFieldState.d.ts +65 -20
- package/dist/calendar/createRangeCalendarState.d.ts +20 -2
- package/dist/calendar/createTimeFieldState.d.ts +38 -67
- package/dist/calendar/index.d.ts +1 -1
- package/dist/calendar/intl/index.d.ts +10 -0
- package/dist/collections/ListCollection.d.ts +8 -0
- package/dist/collections/createListState.d.ts +16 -1
- package/dist/collections/createSelectionState.d.ts +17 -5
- package/dist/collections/getChildNodes.d.ts +19 -0
- package/dist/collections/index.d.ts +3 -2
- package/dist/collections/types.d.ts +9 -0
- package/dist/color/intl/index.d.ts +14 -0
- package/dist/combobox/createComboBoxState.d.ts +7 -0
- package/dist/dnd/createDroppableCollectionState.d.ts +17 -2
- package/dist/dnd/index.d.ts +1 -1
- package/dist/dnd/types.d.ts +3 -1
- package/dist/grid/types.d.ts +17 -3
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4372 -1087
- package/dist/index.js.map +1 -1
- package/dist/select/createSelectState.d.ts +6 -0
- package/dist/selection/Selection.d.ts +16 -0
- package/dist/selection/SelectionManager.d.ts +119 -0
- package/dist/selection/createMultipleSelectionState.d.ts +59 -0
- package/dist/selection/index.d.ts +1 -0
- package/dist/table/TableCollection.d.ts +30 -0
- package/dist/table/createTreeGridState.d.ts +15 -0
- package/dist/table/index.d.ts +2 -1
- package/dist/table/types.d.ts +58 -0
- package/dist/toast/createToastState.d.ts +16 -0
- package/dist/toast/index.d.ts +1 -1
- package/dist/tree/types.d.ts +14 -6
- package/package.json +1 -1
- package/src/calendar/IncompleteDate.ts +430 -0
- package/src/calendar/createCalendarState.ts +129 -40
- package/src/calendar/createDateFieldState.ts +633 -477
- package/src/calendar/createRangeCalendarState.ts +343 -30
- package/src/calendar/createTimeFieldState.ts +179 -467
- package/src/calendar/index.ts +2 -0
- package/src/calendar/intl/index.ts +364 -0
- package/src/collections/ListCollection.ts +31 -0
- package/src/collections/createListState.ts +139 -17
- package/src/collections/createMenuState.ts +3 -0
- package/src/collections/createSelectionState.ts +38 -28
- package/src/collections/getChildNodes.ts +129 -0
- package/src/collections/index.ts +12 -0
- package/src/collections/types.ts +10 -0
- package/src/color/Color.ts +104 -133
- package/src/color/intl/index.ts +1409 -0
- package/src/combobox/createComboBoxState.ts +38 -13
- package/src/dnd/createDroppableCollectionState.ts +125 -6
- package/src/dnd/index.d.ts +1 -0
- package/src/dnd/index.ts +1 -0
- package/src/dnd/types.d.ts +3 -1
- package/src/dnd/types.ts +4 -1
- package/src/grid/createGridState.ts +36 -2
- package/src/grid/types.ts +17 -3
- package/src/index.d.ts +1 -0
- package/src/index.ts +14 -0
- package/src/select/createSelectState.ts +15 -2
- package/src/selection/Selection.ts +28 -0
- package/src/selection/SelectionManager.ts +541 -0
- package/src/selection/createMultipleSelectionState.ts +247 -0
- package/src/selection/index.ts +6 -0
- package/src/steplist/createStepListState.ts +27 -4
- package/src/table/TableCollection.ts +213 -43
- package/src/table/createTableState.ts +20 -0
- package/src/table/createTreeGridState.ts +146 -0
- package/src/table/index.ts +3 -0
- package/src/table/types.ts +66 -0
- package/src/tabs/createTabListState.ts +11 -9
- package/src/toast/createToastState.ts +43 -10
- package/src/toast/index.ts +1 -0
- package/src/tree/createTreeState.ts +37 -6
- package/src/tree/types.ts +14 -6
|
@@ -1,53 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TimeFieldState for Solid-Stately
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
24
|
+
Time,
|
|
25
|
+
type CalendarDate,
|
|
11
26
|
type CalendarDateTime,
|
|
12
27
|
type ZonedDateTime,
|
|
13
28
|
type DateValue,
|
|
14
29
|
getLocalTimeZone,
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type
|
|
24
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
215
|
-
return props.
|
|
252
|
+
get errorMessage() {
|
|
253
|
+
return props.errorMessage;
|
|
216
254
|
},
|
|
217
|
-
get
|
|
218
|
-
return
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
}
|