@proyecto-viviana/solid-stately 0.3.0 → 0.4.0
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 +10 -3
- package/dist/calendar/createRangeCalendarState.d.ts +10 -2
- package/dist/calendar/index.d.ts +1 -1
- package/dist/collections/createListState.d.ts +4 -1
- package/dist/collections/createSelectionState.d.ts +17 -5
- package/dist/collections/getChildNodes.d.ts +11 -0
- package/dist/collections/index.d.ts +2 -1
- package/dist/combobox/createComboBoxState.d.ts +7 -0
- 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 +1570 -425
- 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 +110 -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 +104 -37
- package/src/calendar/createDateFieldState.ts +262 -261
- package/src/calendar/createRangeCalendarState.ts +131 -19
- package/src/calendar/index.ts +2 -0
- package/src/collections/createListState.ts +50 -19
- package/src/collections/createMenuState.ts +3 -0
- package/src/collections/createSelectionState.ts +38 -28
- package/src/collections/getChildNodes.ts +98 -0
- package/src/collections/index.ts +9 -0
- package/src/combobox/createComboBoxState.ts +8 -0
- 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 +11 -0
- package/src/select/createSelectState.ts +15 -2
- package/src/selection/Selection.ts +28 -0
- package/src/selection/SelectionManager.ts +526 -0
- package/src/selection/createMultipleSelectionState.ts +247 -0
- package/src/selection/index.ts +6 -0
- 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
|
@@ -3,22 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides state management for date field components with segment-based editing.
|
|
5
5
|
* Based on @react-stately/datepicker useDateFieldState
|
|
6
|
+
*
|
|
7
|
+
* Editing is modeled with an {@link IncompleteDate} display value so that invalid
|
|
8
|
+
* intermediate dates (e.g. February 31st, produced by editing the day before the
|
|
9
|
+
* month) are held as-typed while the field is focused and only constrained on blur
|
|
10
|
+
* via `confirmPlaceholder`, matching upstream behavior. Out-of-range values are
|
|
11
|
+
* never snapped to `minValue`/`maxValue`; they are kept and reported as invalid.
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
|
-
import { createSignal, createMemo, type Accessor } from "solid-js";
|
|
14
|
+
import { createSignal, createMemo, createEffect, type Accessor } from "solid-js";
|
|
9
15
|
import {
|
|
10
|
-
|
|
16
|
+
type AnyDateTime,
|
|
17
|
+
type Calendar,
|
|
11
18
|
type CalendarDateTime,
|
|
12
19
|
type DateValue,
|
|
20
|
+
GregorianCalendar,
|
|
13
21
|
today,
|
|
14
22
|
getLocalTimeZone,
|
|
15
23
|
DateFormatter,
|
|
16
24
|
toCalendarDate as intlToCalendarDate,
|
|
17
25
|
toCalendarDateTime,
|
|
18
|
-
endOfMonth,
|
|
19
26
|
} from "@internationalized/date";
|
|
20
27
|
import { access, type MaybeAccessor } from "../utils";
|
|
21
28
|
import type { ValidationState } from "./createCalendarState";
|
|
29
|
+
import { IncompleteDate } from "./IncompleteDate";
|
|
22
30
|
import {
|
|
23
31
|
createFormValidationState,
|
|
24
32
|
DEFAULT_VALIDATION_RESULT,
|
|
@@ -47,7 +55,7 @@ export interface DateSegment {
|
|
|
47
55
|
/** The text content of the segment. */
|
|
48
56
|
text: string;
|
|
49
57
|
/** The numeric value of the segment (if applicable). */
|
|
50
|
-
value?: number;
|
|
58
|
+
value?: number | null;
|
|
51
59
|
/** The minimum value for the segment. */
|
|
52
60
|
minValue?: number;
|
|
53
61
|
/** The maximum value for the segment. */
|
|
@@ -160,62 +168,53 @@ export interface DateFieldState<T extends DateValue = DateValue> {
|
|
|
160
168
|
defaultValue: T | null;
|
|
161
169
|
}
|
|
162
170
|
|
|
171
|
+
type HourCycle = "h11" | "h12" | "h23" | "h24";
|
|
172
|
+
|
|
163
173
|
/**
|
|
164
174
|
* Provides state management for a date field component.
|
|
165
175
|
*/
|
|
166
|
-
export function createDateFieldState<T extends DateValue =
|
|
176
|
+
export function createDateFieldState<T extends DateValue = DateValue>(
|
|
167
177
|
props: DateFieldStateProps<T> = {},
|
|
168
178
|
): DateFieldState<T> {
|
|
169
179
|
const timeZone = getLocalTimeZone();
|
|
170
180
|
const locale = props.locale ?? "en-US";
|
|
171
181
|
const granularity = props.granularity ?? "day";
|
|
182
|
+
const calendar: Calendar = new GregorianCalendar();
|
|
183
|
+
const hourCycle = resolveHourCycle(locale, props.hourCycle);
|
|
184
|
+
const displaySegments = computeDisplaySegments(granularity, hourCycle);
|
|
185
|
+
|
|
186
|
+
const getPlaceholderValue = (): DateValue => {
|
|
187
|
+
const base = props.placeholderValue ?? today(timeZone);
|
|
188
|
+
return granularity === "day" ? intlToCalendarDate(base) : toCalendarDateTime(base);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const toSource = (v: DateValue | null): Partial<Omit<AnyDateTime, "copy">> | null =>
|
|
192
|
+
(v as Partial<Omit<AnyDateTime, "copy">> | null) ?? null;
|
|
172
193
|
|
|
173
194
|
// State signals
|
|
174
195
|
const initialValue = props.defaultValue ?? null;
|
|
175
196
|
const [internalValue, setInternalValue] = createSignal<T | null>(initialValue);
|
|
176
197
|
|
|
177
|
-
//
|
|
178
|
-
const [placeholderDate, setPlaceholderDate] = createSignal<Partial<DateParts>>({});
|
|
179
|
-
|
|
180
|
-
// Controlled vs uncontrolled value
|
|
198
|
+
// Controlled vs uncontrolled committed value
|
|
181
199
|
const value = createMemo<T | null>(() => {
|
|
182
200
|
const controlled = access(props.value);
|
|
183
201
|
return controlled !== undefined ? controlled : internalValue();
|
|
184
202
|
});
|
|
185
203
|
|
|
186
|
-
// The
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// Build from placeholder parts
|
|
192
|
-
const parts = placeholderDate();
|
|
193
|
-
const placeholder = props.placeholderValue ?? today(timeZone);
|
|
194
|
-
|
|
195
|
-
if (Object.keys(parts).length === 0) return null;
|
|
196
|
-
|
|
197
|
-
// Create a date from the parts
|
|
198
|
-
const year = parts.year ?? placeholder.year;
|
|
199
|
-
const month = parts.month ?? placeholder.month;
|
|
200
|
-
const day = parts.day ?? placeholder.day;
|
|
204
|
+
// The display override: holds the (possibly invalid/incomplete) value being edited.
|
|
205
|
+
const [displayValue, setDisplayValue] = createSignal<IncompleteDate>(
|
|
206
|
+
new IncompleteDate(calendar, hourCycle, toSource(value())),
|
|
207
|
+
);
|
|
201
208
|
|
|
202
|
-
|
|
203
|
-
|
|
209
|
+
// Reset the display override whenever the committed value changes from the outside
|
|
210
|
+
// (e.g. a controlled `value` prop update).
|
|
211
|
+
let lastSyncedValue = value();
|
|
212
|
+
createEffect(() => {
|
|
213
|
+
const v = value();
|
|
214
|
+
if (v !== lastSyncedValue) {
|
|
215
|
+
lastSyncedValue = v;
|
|
216
|
+
setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(v)));
|
|
204
217
|
}
|
|
205
|
-
|
|
206
|
-
// For time granularities
|
|
207
|
-
const hour = parts.hour ?? 0;
|
|
208
|
-
const minute = parts.minute ?? 0;
|
|
209
|
-
const second = parts.second ?? 0;
|
|
210
|
-
|
|
211
|
-
return toCalendarDateTime(placeholder).set({
|
|
212
|
-
year,
|
|
213
|
-
month,
|
|
214
|
-
day,
|
|
215
|
-
hour,
|
|
216
|
-
minute,
|
|
217
|
-
second,
|
|
218
|
-
}) as unknown as T;
|
|
219
218
|
});
|
|
220
219
|
|
|
221
220
|
// Derived states
|
|
@@ -284,187 +283,184 @@ export function createDateFieldState<T extends DateValue = CalendarDate>(
|
|
|
284
283
|
() => explicitValidationState() ?? (isInvalid() ? "invalid" : undefined),
|
|
285
284
|
);
|
|
286
285
|
|
|
287
|
-
//
|
|
288
|
-
const
|
|
286
|
+
// The effective date value, derived from the display override.
|
|
287
|
+
const dateValue = createMemo<DateValue | null>(() => {
|
|
289
288
|
const v = value();
|
|
290
|
-
|
|
291
|
-
const
|
|
289
|
+
if (v) return v;
|
|
290
|
+
const dv = displayValue();
|
|
291
|
+
if (dv.isCleared(displaySegments)) return null;
|
|
292
|
+
return dv.toValue(getPlaceholderValue());
|
|
293
|
+
});
|
|
292
294
|
|
|
293
|
-
|
|
295
|
+
// Generate segments from the display override (so invalid intermediate values render as typed).
|
|
296
|
+
const segments = createMemo<DateSegment[]>(() => {
|
|
297
|
+
const dv = displayValue();
|
|
298
|
+
const base = value() ?? getPlaceholderValue();
|
|
299
|
+
const resolved = dv.toValue(base);
|
|
300
|
+
const jsDate = resolved.toDate(timeZone);
|
|
294
301
|
|
|
295
|
-
// Determine format options based on granularity
|
|
296
302
|
const shouldForceLeadingZeros = props.shouldForceLeadingZeros === true;
|
|
297
|
-
const formatOptions
|
|
298
|
-
year: "numeric",
|
|
299
|
-
month: shouldForceLeadingZeros ? "2-digit" : "numeric",
|
|
300
|
-
day: shouldForceLeadingZeros ? "2-digit" : "numeric",
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
if (granularity !== "day") {
|
|
304
|
-
formatOptions.hour = "2-digit";
|
|
305
|
-
formatOptions.minute = "2-digit";
|
|
306
|
-
if (granularity === "second") {
|
|
307
|
-
formatOptions.second = "2-digit";
|
|
308
|
-
}
|
|
309
|
-
if (props.hourCycle) {
|
|
310
|
-
formatOptions.hourCycle = props.hourCycle === 12 ? "h12" : "h23";
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
303
|
+
const formatOptions = buildFormatOptions(granularity, shouldForceLeadingZeros, props.hourCycle);
|
|
314
304
|
const formatter = new DateFormatter(locale, formatOptions);
|
|
315
|
-
const
|
|
316
|
-
const
|
|
305
|
+
const resolvedOptions = formatter.resolvedOptions() as unknown as Record<string, string>;
|
|
306
|
+
const parts = formatter.formatToParts(jsDate);
|
|
317
307
|
|
|
318
|
-
|
|
308
|
+
const numberFormatter = new Intl.NumberFormat(locale, { useGrouping: false });
|
|
309
|
+
const twoDigitFormatter = new Intl.NumberFormat(locale, {
|
|
310
|
+
useGrouping: false,
|
|
311
|
+
minimumIntegerDigits: 2,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const result: DateSegment[] = [];
|
|
315
|
+
for (const part of parts) {
|
|
319
316
|
const type = mapPartType(part.type);
|
|
320
317
|
|
|
321
318
|
if (type === "literal") {
|
|
322
|
-
|
|
319
|
+
result.push({
|
|
323
320
|
type: "literal",
|
|
324
321
|
text: part.value,
|
|
325
322
|
isEditable: false,
|
|
326
323
|
isPlaceholder: false,
|
|
327
324
|
placeholder: part.value,
|
|
328
325
|
});
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (!type) continue;
|
|
329
|
+
|
|
330
|
+
const editable = isEditableSegment(type);
|
|
331
|
+
const rawValue = rawSegmentValue(dv, type);
|
|
332
|
+
const isPlaceholder = editable && rawValue == null;
|
|
333
|
+
const limits = dv.getSegmentLimits(type);
|
|
334
|
+
const placeholderText = getPlaceholderText(
|
|
335
|
+
type,
|
|
336
|
+
locale,
|
|
337
|
+
granularity,
|
|
338
|
+
shouldForceLeadingZeros,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// For numeric date/time segments, render the raw edited value (e.g. "31" even
|
|
342
|
+
// when the resolved date is Feb 28) using a NumberFormatter, mirroring upstream.
|
|
343
|
+
let text = part.value;
|
|
344
|
+
if (
|
|
345
|
+
(type === "year" || type === "month" || type === "day" || type === "hour") &&
|
|
346
|
+
typeof rawValue === "number"
|
|
347
|
+
) {
|
|
348
|
+
const numeric = rawValue ?? 0;
|
|
349
|
+
text =
|
|
350
|
+
resolvedOptions[type] === "2-digit"
|
|
351
|
+
? twoDigitFormatter.format(numeric)
|
|
352
|
+
: numberFormatter.format(numeric);
|
|
346
353
|
}
|
|
354
|
+
|
|
355
|
+
result.push({
|
|
356
|
+
type,
|
|
357
|
+
text: isPlaceholder ? placeholderText : text,
|
|
358
|
+
value: limits?.value,
|
|
359
|
+
minValue: limits?.minValue,
|
|
360
|
+
maxValue: limits?.maxValue,
|
|
361
|
+
isEditable: editable && !isDisabled() && !isReadOnly(),
|
|
362
|
+
isPlaceholder,
|
|
363
|
+
placeholder: placeholderText,
|
|
364
|
+
});
|
|
347
365
|
}
|
|
348
366
|
|
|
349
|
-
return
|
|
367
|
+
return result;
|
|
350
368
|
});
|
|
351
369
|
|
|
352
|
-
//
|
|
353
|
-
const
|
|
354
|
-
if (isDisabled() || isReadOnly()) return;
|
|
355
|
-
|
|
370
|
+
// Commit a concrete value (updates the controlled/uncontrolled value and fires onChange).
|
|
371
|
+
const commitDate = (newValue: T | null) => {
|
|
356
372
|
const controlled = access(props.value);
|
|
357
373
|
if (controlled === undefined) {
|
|
374
|
+
lastSyncedValue = newValue;
|
|
358
375
|
setInternalValue(() => newValue);
|
|
359
376
|
}
|
|
360
|
-
|
|
361
|
-
if (props.onChange) {
|
|
362
|
-
props.onChange(newValue);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Clear placeholder parts
|
|
366
|
-
setPlaceholderDate({});
|
|
377
|
+
props.onChange?.(newValue);
|
|
367
378
|
};
|
|
368
379
|
|
|
369
|
-
// Set
|
|
370
|
-
|
|
380
|
+
// Set value with the upstream split: complete + valid commits eagerly; an incomplete
|
|
381
|
+
// or invalid (e.g. Feb 31) value is held as a display override until blur.
|
|
382
|
+
const setValue = (newValue: T | IncompleteDate | null) => {
|
|
371
383
|
if (isDisabled() || isReadOnly()) return;
|
|
372
384
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
// Update placeholder parts
|
|
381
|
-
setPlaceholderDate((prev) => ({
|
|
382
|
-
...prev,
|
|
383
|
-
[type]: newValue,
|
|
384
|
-
}));
|
|
385
|
+
if (
|
|
386
|
+
newValue == null ||
|
|
387
|
+
(newValue instanceof IncompleteDate && newValue.isCleared(displaySegments))
|
|
388
|
+
) {
|
|
389
|
+
commitDate(null);
|
|
390
|
+
setDisplayValue(new IncompleteDate(calendar, hourCycle, null));
|
|
391
|
+
return;
|
|
385
392
|
}
|
|
386
|
-
};
|
|
387
393
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
394
|
+
if (!(newValue instanceof IncompleteDate)) {
|
|
395
|
+
commitDate(newValue);
|
|
396
|
+
setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(newValue)));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
391
399
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
400
|
+
// If the new value is complete and valid, trigger onChange eagerly. If it represents
|
|
401
|
+
// an incomplete or invalid value (e.g. February 30th), wait until blur to trigger onChange.
|
|
402
|
+
if (newValue.isComplete(displaySegments)) {
|
|
403
|
+
const resolved = newValue.toValue(value() ?? getPlaceholderValue());
|
|
404
|
+
if (newValue.validate(resolved, displaySegments)) {
|
|
405
|
+
const committed = value();
|
|
406
|
+
if (!committed || resolved.compare(committed) !== 0) {
|
|
407
|
+
setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(resolved)));
|
|
408
|
+
commitDate(resolved as T);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
396
413
|
|
|
397
|
-
|
|
398
|
-
|
|
414
|
+
// Incomplete/invalid value. Set temporary display override.
|
|
415
|
+
setDisplayValue(newValue);
|
|
399
416
|
};
|
|
400
417
|
|
|
401
|
-
|
|
402
|
-
const decrementSegment = (type: DateSegmentType) => {
|
|
418
|
+
const adjustSegment = (type: DateSegmentType, amount: number) => {
|
|
403
419
|
if (isDisabled() || isReadOnly()) return;
|
|
420
|
+
setValue(displayValue().cycle(type, amount, getPlaceholderValue(), displaySegments));
|
|
421
|
+
};
|
|
404
422
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const newValue = current !== undefined ? current - 1 : max;
|
|
411
|
-
setSegment(type, newValue < min ? max : newValue);
|
|
423
|
+
// Set a specific segment value
|
|
424
|
+
const setSegment = (type: DateSegmentType, newValue: number) => {
|
|
425
|
+
if (isDisabled() || isReadOnly()) return;
|
|
426
|
+
setValue(displayValue().set(type, newValue, getPlaceholderValue()));
|
|
412
427
|
};
|
|
413
428
|
|
|
414
|
-
|
|
429
|
+
const incrementSegment = (type: DateSegmentType) => adjustSegment(type, 1);
|
|
430
|
+
const decrementSegment = (type: DateSegmentType) => adjustSegment(type, -1);
|
|
431
|
+
|
|
432
|
+
// Clear a segment, reverting it to its placeholder.
|
|
415
433
|
const clearSegment = (type: DateSegmentType) => {
|
|
416
434
|
if (isDisabled() || isReadOnly()) return;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (v) {
|
|
421
|
-
// Reset only the target segment to its minimum value instead of clearing the entire value
|
|
422
|
-
const min = getMinValue(type);
|
|
423
|
-
const updated = updateDatePart(v, type, min);
|
|
424
|
-
setValue(updated as T);
|
|
425
|
-
} else {
|
|
426
|
-
setPlaceholderDate((prev) => {
|
|
427
|
-
const next = { ...prev };
|
|
428
|
-
delete next[type as keyof DateParts];
|
|
429
|
-
return next;
|
|
430
|
-
});
|
|
435
|
+
let dv = displayValue();
|
|
436
|
+
if (type !== "timeZoneName" && type !== "literal") {
|
|
437
|
+
dv = dv.clear(type);
|
|
431
438
|
}
|
|
439
|
+
setValue(dv);
|
|
432
440
|
};
|
|
433
441
|
|
|
434
|
-
//
|
|
442
|
+
// When the field is blurred, constrain a complete-but-invalid display value
|
|
443
|
+
// (e.g. February 31st -> February 28th) and emit onChange. Out-of-range values
|
|
444
|
+
// are left as-is and reported via validation rather than being snapped.
|
|
435
445
|
const confirmPlaceholder = () => {
|
|
436
446
|
if (isDisabled() || isReadOnly()) return;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
return maxValue;
|
|
446
|
-
}
|
|
447
|
-
return candidate;
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
const parts = placeholderDate();
|
|
451
|
-
if (Object.keys(parts).length > 0) {
|
|
452
|
-
const dv = dateValue();
|
|
453
|
-
if (dv) {
|
|
454
|
-
const candidate = constrain(dv);
|
|
455
|
-
// Commit even if unavailable so isInvalid can flag it; don't leave field stuck
|
|
456
|
-
setValue(candidate as T);
|
|
457
|
-
}
|
|
458
|
-
return;
|
|
447
|
+
const dv = displayValue();
|
|
448
|
+
if (!dv.isComplete(displaySegments)) return;
|
|
449
|
+
|
|
450
|
+
const resolved = dv.toValue(value() ?? getPlaceholderValue());
|
|
451
|
+
const committed = value();
|
|
452
|
+
const needsCommit = !committed || resolved.compare(committed) !== 0;
|
|
453
|
+
if (needsCommit) {
|
|
454
|
+
commitDate(resolved as T);
|
|
459
455
|
}
|
|
460
456
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
457
|
+
// Reset the display override so a constrained value (e.g. February 31st ->
|
|
458
|
+
// February 28th) renders. Only touch the signal when the displayed raw value
|
|
459
|
+
// diverged from the resolved value, or when we committed — otherwise leave it
|
|
460
|
+
// untouched so blur (which fires on every focus move) does not churn the
|
|
461
|
+
// segments and drop focus.
|
|
462
|
+
if (needsCommit || !dv.validate(resolved, displaySegments)) {
|
|
463
|
+
setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(value() ?? resolved)));
|
|
468
464
|
}
|
|
469
465
|
};
|
|
470
466
|
|
|
@@ -519,99 +515,124 @@ export function createDateFieldState<T extends DateValue = CalendarDate>(
|
|
|
519
515
|
};
|
|
520
516
|
}
|
|
521
517
|
|
|
522
|
-
|
|
523
|
-
year
|
|
524
|
-
month
|
|
525
|
-
day
|
|
526
|
-
hour
|
|
527
|
-
minute
|
|
528
|
-
second
|
|
518
|
+
const EDITABLE_SEGMENTS: Record<string, boolean> = {
|
|
519
|
+
year: true,
|
|
520
|
+
month: true,
|
|
521
|
+
day: true,
|
|
522
|
+
hour: true,
|
|
523
|
+
minute: true,
|
|
524
|
+
second: true,
|
|
525
|
+
dayPeriod: true,
|
|
526
|
+
era: true,
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
function isEditableSegment(type: DateSegmentType): boolean {
|
|
530
|
+
return EDITABLE_SEGMENTS[type] === true;
|
|
529
531
|
}
|
|
530
532
|
|
|
531
|
-
function
|
|
533
|
+
function rawSegmentValue(dv: IncompleteDate, type: DateSegmentType): number | string | null {
|
|
532
534
|
switch (type) {
|
|
535
|
+
case "era":
|
|
536
|
+
return dv.era;
|
|
533
537
|
case "year":
|
|
534
|
-
return
|
|
538
|
+
return dv.year;
|
|
535
539
|
case "month":
|
|
536
|
-
return
|
|
540
|
+
return dv.month;
|
|
537
541
|
case "day":
|
|
538
|
-
return
|
|
542
|
+
return dv.day;
|
|
539
543
|
case "hour":
|
|
540
|
-
return
|
|
544
|
+
return dv.hour;
|
|
541
545
|
case "minute":
|
|
542
|
-
return
|
|
546
|
+
return dv.minute;
|
|
543
547
|
case "second":
|
|
544
|
-
return
|
|
548
|
+
return dv.second;
|
|
545
549
|
case "dayPeriod":
|
|
546
|
-
return
|
|
547
|
-
case "era":
|
|
548
|
-
return "era";
|
|
549
|
-
case "timeZoneName":
|
|
550
|
-
return "timeZoneName";
|
|
551
|
-
case "literal":
|
|
552
|
-
return "literal";
|
|
550
|
+
return dv.dayPeriod;
|
|
553
551
|
default:
|
|
554
552
|
return null;
|
|
555
553
|
}
|
|
556
554
|
}
|
|
557
555
|
|
|
558
|
-
function
|
|
559
|
-
|
|
556
|
+
function resolveHourCycle(locale: string, hourCycle?: 12 | 24): HourCycle {
|
|
557
|
+
const formatter = new DateFormatter(locale, {
|
|
558
|
+
hour: "numeric",
|
|
559
|
+
hour12: hourCycle != null ? hourCycle === 12 : undefined,
|
|
560
|
+
});
|
|
561
|
+
return (formatter.resolvedOptions().hourCycle as HourCycle) ?? "h23";
|
|
562
|
+
}
|
|
560
563
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
564
|
+
function computeDisplaySegments(
|
|
565
|
+
granularity: "day" | "hour" | "minute" | "second",
|
|
566
|
+
hourCycle: HourCycle,
|
|
567
|
+
): DateSegmentType[] {
|
|
568
|
+
const is12HourClock = hourCycle === "h11" || hourCycle === "h12";
|
|
569
|
+
const segments: DateSegmentType[] = [
|
|
570
|
+
"era",
|
|
571
|
+
"year",
|
|
572
|
+
"month",
|
|
573
|
+
"day",
|
|
574
|
+
"hour",
|
|
575
|
+
...(is12HourClock ? (["dayPeriod"] as const) : []),
|
|
576
|
+
"minute",
|
|
577
|
+
"second",
|
|
578
|
+
];
|
|
579
|
+
// We never expose era as an editable segment, so the maximum displayed unit is the year.
|
|
580
|
+
const minIndex = segments.indexOf("era");
|
|
581
|
+
const maxIndex = segments.indexOf(
|
|
582
|
+
granularity === "hour" && is12HourClock ? "dayPeriod" : granularity,
|
|
583
|
+
);
|
|
584
|
+
return segments.slice(minIndex, maxIndex + 1);
|
|
577
585
|
}
|
|
578
586
|
|
|
579
|
-
function
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
587
|
+
function buildFormatOptions(
|
|
588
|
+
granularity: "day" | "hour" | "minute" | "second",
|
|
589
|
+
shouldForceLeadingZeros: boolean,
|
|
590
|
+
hourCycle?: 12 | 24,
|
|
591
|
+
): Intl.DateTimeFormatOptions {
|
|
592
|
+
const formatOptions: Intl.DateTimeFormatOptions = {
|
|
593
|
+
year: "numeric",
|
|
594
|
+
month: shouldForceLeadingZeros ? "2-digit" : "numeric",
|
|
595
|
+
day: shouldForceLeadingZeros ? "2-digit" : "numeric",
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
if (granularity !== "day") {
|
|
599
|
+
formatOptions.hour = "2-digit";
|
|
600
|
+
formatOptions.minute = "2-digit";
|
|
601
|
+
if (granularity === "second") {
|
|
602
|
+
formatOptions.second = "2-digit";
|
|
603
|
+
}
|
|
604
|
+
if (hourCycle) {
|
|
605
|
+
formatOptions.hourCycle = hourCycle === 12 ? "h12" : "h23";
|
|
606
|
+
}
|
|
595
607
|
}
|
|
608
|
+
|
|
609
|
+
return formatOptions;
|
|
596
610
|
}
|
|
597
611
|
|
|
598
|
-
function
|
|
612
|
+
function mapPartType(type: Intl.DateTimeFormatPartTypes): DateSegmentType | null {
|
|
599
613
|
switch (type) {
|
|
600
614
|
case "year":
|
|
601
|
-
return
|
|
615
|
+
return "year";
|
|
602
616
|
case "month":
|
|
603
|
-
return
|
|
617
|
+
return "month";
|
|
604
618
|
case "day":
|
|
605
|
-
|
|
606
|
-
return endOfMonth(date).day;
|
|
619
|
+
return "day";
|
|
607
620
|
case "hour":
|
|
608
|
-
return
|
|
621
|
+
return "hour";
|
|
609
622
|
case "minute":
|
|
610
|
-
return
|
|
623
|
+
return "minute";
|
|
611
624
|
case "second":
|
|
612
|
-
return
|
|
625
|
+
return "second";
|
|
626
|
+
case "dayPeriod":
|
|
627
|
+
return "dayPeriod";
|
|
628
|
+
case "era":
|
|
629
|
+
return "era";
|
|
630
|
+
case "timeZoneName":
|
|
631
|
+
return "timeZoneName";
|
|
632
|
+
case "literal":
|
|
633
|
+
return "literal";
|
|
613
634
|
default:
|
|
614
|
-
return
|
|
635
|
+
return null;
|
|
615
636
|
}
|
|
616
637
|
}
|
|
617
638
|
|
|
@@ -646,7 +667,7 @@ function getPlaceholderText(
|
|
|
646
667
|
for (const part of parts) {
|
|
647
668
|
const mappedType = mapPartType(part.type);
|
|
648
669
|
if (mappedType === type) {
|
|
649
|
-
return part.value.replace(/\d/g, (
|
|
670
|
+
return part.value.replace(/\d/g, (_d) => {
|
|
650
671
|
if (type === "year") return "y";
|
|
651
672
|
if (type === "month") return "m";
|
|
652
673
|
if (type === "day") return "d";
|
|
@@ -673,25 +694,5 @@ function getPlaceholderText(
|
|
|
673
694
|
}
|
|
674
695
|
}
|
|
675
696
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
return date;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
switch (type) {
|
|
682
|
-
case "year":
|
|
683
|
-
return date.set({ year: value });
|
|
684
|
-
case "month":
|
|
685
|
-
return date.set({ month: value });
|
|
686
|
-
case "day":
|
|
687
|
-
return date.set({ day: value });
|
|
688
|
-
case "hour":
|
|
689
|
-
return date.set({ hour: value });
|
|
690
|
-
case "minute":
|
|
691
|
-
return date.set({ minute: value });
|
|
692
|
-
case "second":
|
|
693
|
-
return date.set({ second: value });
|
|
694
|
-
default:
|
|
695
|
-
return date;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
697
|
+
// Re-export so existing imports of CalendarDateTime keep resolving through this module.
|
|
698
|
+
export type { CalendarDateTime };
|