@proyecto-viviana/solid-stately 0.2.7 → 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/README.md +30 -283
- package/dist/autocomplete/createAutocompleteState.d.ts +2 -2
- package/dist/autocomplete/index.d.ts +1 -1
- package/dist/calendar/IncompleteDate.d.ts +61 -0
- package/dist/calendar/createCalendarState.d.ts +41 -16
- package/dist/calendar/createDateFieldState.d.ts +39 -9
- package/dist/calendar/createDatePickerState.d.ts +60 -0
- package/dist/calendar/createRangeCalendarState.d.ts +37 -10
- package/dist/calendar/createTimeFieldState.d.ts +34 -7
- package/dist/calendar/index.d.ts +7 -6
- package/dist/checkbox/createCheckboxGroupState.d.ts +6 -6
- package/dist/checkbox/index.d.ts +1 -1
- package/dist/collections/ListCollection.d.ts +1 -1
- package/dist/collections/createListState.d.ts +11 -8
- package/dist/collections/createMenuState.d.ts +7 -7
- package/dist/collections/createSelectionState.d.ts +22 -10
- package/dist/collections/getChildNodes.d.ts +11 -0
- package/dist/collections/index.d.ts +6 -5
- package/dist/collections/types.d.ts +12 -12
- package/dist/color/Color.d.ts +1 -1
- package/dist/color/createColorAreaState.d.ts +4 -2
- package/dist/color/createColorFieldState.d.ts +38 -12
- package/dist/color/createColorSliderState.d.ts +9 -2
- package/dist/color/createColorWheelState.d.ts +13 -2
- package/dist/color/getColorChannels.d.ts +1 -1
- package/dist/color/index.d.ts +6 -6
- package/dist/color/types.d.ts +4 -4
- package/dist/combobox/createComboBoxState.d.ts +34 -13
- package/dist/combobox/index.d.ts +1 -1
- package/dist/data/createAsyncList.d.ts +4 -4
- package/dist/data/createListData.d.ts +2 -2
- package/dist/data/createTreeData.d.ts +6 -2
- package/dist/data/index.d.ts +3 -3
- package/dist/datepicker/index.d.ts +2 -4
- package/dist/disclosure/createDisclosureState.d.ts +2 -2
- package/dist/disclosure/index.d.ts +1 -1
- package/dist/dnd/createDragState.d.ts +2 -2
- package/dist/dnd/createDraggableCollectionState.d.ts +2 -2
- package/dist/dnd/createDropState.d.ts +2 -2
- package/dist/dnd/createDroppableCollectionState.d.ts +3 -3
- package/dist/dnd/index.d.ts +5 -5
- package/dist/dnd/types.d.ts +19 -17
- package/dist/form/createFormValidationState.d.ts +3 -3
- package/dist/form/index.d.ts +1 -1
- package/dist/grid/createGridState.d.ts +2 -2
- package/dist/grid/index.d.ts +2 -2
- package/dist/grid/types.d.ts +27 -11
- package/dist/index.d.ts +31 -30
- package/dist/index.js +9077 -7246
- package/dist/index.js.map +1 -1
- package/dist/menu/index.d.ts +2 -2
- package/dist/numberfield/createNumberFieldState.d.ts +2 -2
- package/dist/numberfield/index.d.ts +1 -1
- package/dist/overlays/createOverlayTriggerState.d.ts +2 -2
- package/dist/overlays/index.d.ts +1 -1
- package/dist/radio/createRadioGroupState.d.ts +7 -7
- package/dist/radio/index.d.ts +1 -1
- package/dist/searchfield/createSearchFieldState.d.ts +2 -2
- package/dist/searchfield/index.d.ts +2 -2
- package/dist/select/createSelectState.d.ts +14 -8
- package/dist/select/index.d.ts +1 -1
- 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 +6 -5
- package/dist/slider/createSliderState.d.ts +3 -3
- package/dist/slider/index.d.ts +2 -2
- package/dist/steplist/createStepListState.d.ts +48 -0
- package/dist/steplist/index.d.ts +1 -0
- package/dist/table/TableCollection.d.ts +33 -3
- package/dist/table/createTableColumnResizeState.d.ts +54 -0
- package/dist/table/createTableState.d.ts +2 -2
- package/dist/table/createTreeGridState.d.ts +15 -0
- package/dist/table/index.d.ts +6 -3
- package/dist/table/types.d.ts +68 -10
- package/dist/tabs/createTabListState.d.ts +5 -5
- package/dist/tabs/index.d.ts +1 -1
- package/dist/textfield/createTextFieldState.d.ts +2 -2
- package/dist/textfield/index.d.ts +1 -1
- package/dist/toast/createToastState.d.ts +24 -2
- package/dist/toast/index.d.ts +1 -1
- package/dist/toggle/createToggleGroupState.d.ts +4 -4
- package/dist/toggle/createToggleState.d.ts +2 -2
- package/dist/toggle/index.d.ts +2 -2
- package/dist/tooltip/createTooltipTriggerState.d.ts +2 -2
- package/dist/tooltip/index.d.ts +1 -1
- package/dist/tree/TreeCollection.d.ts +2 -2
- package/dist/tree/createTreeState.d.ts +2 -2
- package/dist/tree/index.d.ts +3 -3
- package/dist/tree/types.d.ts +27 -13
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/reactivity.d.ts +1 -1
- package/package.json +29 -30
- package/src/autocomplete/createAutocompleteState.d.ts +19 -17
- package/src/autocomplete/createAutocompleteState.ts +24 -30
- package/src/autocomplete/index.d.ts +6 -2
- package/src/autocomplete/index.ts +1 -1
- package/src/calendar/IncompleteDate.ts +430 -0
- package/src/calendar/createCalendarState.d.ts +139 -117
- package/src/calendar/createCalendarState.ts +308 -94
- package/src/calendar/createDateFieldState.d.ts +130 -95
- package/src/calendar/createDateFieldState.ts +447 -334
- package/src/calendar/createDatePickerState.ts +347 -0
- package/src/calendar/createRangeCalendarState.d.ts +154 -129
- package/src/calendar/createRangeCalendarState.ts +411 -85
- package/src/calendar/createTimeFieldState.d.ts +81 -79
- package/src/calendar/createTimeFieldState.ts +187 -108
- package/src/calendar/index.d.ts +80 -7
- package/src/calendar/index.ts +23 -12
- package/src/checkbox/createCheckboxGroupState.d.ts +57 -55
- package/src/checkbox/createCheckboxGroupState.ts +20 -29
- package/src/checkbox/index.d.ts +6 -2
- package/src/checkbox/index.ts +1 -1
- package/src/collections/ListCollection.d.ts +25 -22
- package/src/collections/ListCollection.ts +6 -6
- package/src/collections/createListState.d.ts +73 -58
- package/src/collections/createListState.ts +67 -35
- package/src/collections/createMenuState.d.ts +36 -31
- package/src/collections/createMenuState.ts +21 -12
- package/src/collections/createSelectionState.d.ts +78 -65
- package/src/collections/createSelectionState.ts +66 -52
- package/src/collections/getChildNodes.ts +98 -0
- package/src/collections/index.d.ts +38 -6
- package/src/collections/index.ts +14 -10
- package/src/collections/types.d.ts +101 -101
- package/src/collections/types.ts +12 -12
- package/src/color/Color.d.ts +20 -5
- package/src/color/Color.ts +357 -254
- package/src/color/createColorAreaState.d.ts +67 -65
- package/src/color/createColorAreaState.ts +21 -13
- package/src/color/createColorFieldState.d.ts +46 -44
- package/src/color/createColorFieldState.ts +285 -149
- package/src/color/createColorSliderState.d.ts +58 -56
- package/src/color/createColorSliderState.ts +45 -14
- package/src/color/createColorWheelState.d.ts +42 -40
- package/src/color/createColorWheelState.ts +101 -64
- package/src/color/getColorChannels.ts +7 -5
- package/src/color/index.d.ts +36 -7
- package/src/color/index.ts +6 -9
- package/src/color/types.d.ts +68 -63
- package/src/color/types.ts +11 -19
- package/src/combobox/createComboBoxState.d.ts +107 -105
- package/src/combobox/createComboBoxState.ts +184 -89
- package/src/combobox/index.d.ts +10 -2
- package/src/combobox/index.ts +1 -1
- package/src/data/createAsyncList.ts +89 -63
- package/src/data/createListData.ts +67 -67
- package/src/data/createTreeData.ts +304 -91
- package/src/data/index.ts +3 -3
- package/src/datepicker/index.ts +6 -10
- package/src/disclosure/createDisclosureState.d.ts +47 -43
- package/src/disclosure/createDisclosureState.ts +7 -21
- package/src/disclosure/index.d.ts +10 -2
- package/src/disclosure/index.ts +1 -1
- package/src/dnd/createDragState.d.ts +51 -42
- package/src/dnd/createDragState.ts +12 -14
- package/src/dnd/createDraggableCollectionState.d.ts +52 -41
- package/src/dnd/createDraggableCollectionState.ts +14 -21
- package/src/dnd/createDropState.d.ts +64 -45
- package/src/dnd/createDropState.ts +22 -32
- package/src/dnd/createDroppableCollectionState.d.ts +88 -59
- package/src/dnd/createDroppableCollectionState.ts +40 -43
- package/src/dnd/index.d.ts +55 -6
- package/src/dnd/index.ts +6 -18
- package/src/dnd/types.d.ts +170 -157
- package/src/dnd/types.ts +24 -22
- package/src/form/createFormValidationState.d.ts +47 -45
- package/src/form/createFormValidationState.ts +30 -63
- package/src/form/index.d.ts +16 -2
- package/src/form/index.ts +1 -1
- package/src/grid/createGridState.d.ts +7 -4
- package/src/grid/createGridState.ts +78 -51
- package/src/grid/index.d.ts +3 -3
- package/src/grid/index.ts +2 -8
- package/src/grid/types.d.ts +136 -129
- package/src/grid/types.ts +34 -18
- package/src/index.d.ts +311 -26
- package/src/index.ts +59 -89
- package/src/menu/index.ts +2 -4
- package/src/numberfield/createNumberFieldState.d.ts +56 -54
- package/src/numberfield/createNumberFieldState.ts +26 -31
- package/src/numberfield/index.d.ts +6 -2
- package/src/numberfield/index.ts +1 -1
- package/src/overlays/createOverlayTriggerState.d.ts +22 -20
- package/src/overlays/createOverlayTriggerState.ts +3 -4
- package/src/overlays/index.d.ts +6 -2
- package/src/overlays/index.ts +1 -1
- package/src/radio/createRadioGroupState.d.ts +54 -52
- package/src/radio/createRadioGroupState.ts +27 -29
- package/src/radio/index.d.ts +7 -2
- package/src/radio/index.ts +1 -1
- package/src/searchfield/createSearchFieldState.d.ts +16 -14
- package/src/searchfield/createSearchFieldState.ts +5 -11
- package/src/searchfield/index.d.ts +3 -3
- package/src/searchfield/index.ts +2 -5
- package/src/select/createSelectState.d.ts +63 -61
- package/src/select/createSelectState.ts +42 -29
- package/src/select/index.d.ts +2 -2
- package/src/select/index.ts +1 -5
- 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 +12 -7
- package/src/slider/createSliderState.d.ts +60 -60
- package/src/slider/createSliderState.ts +55 -33
- package/src/slider/index.d.ts +3 -3
- package/src/slider/index.ts +2 -6
- package/src/ssr/index.d.ts +1 -1
- package/src/ssr/index.ts +2 -2
- package/src/steplist/createStepListState.ts +191 -0
- package/src/steplist/index.ts +5 -0
- package/src/table/TableCollection.d.ts +42 -40
- package/src/table/TableCollection.ts +266 -99
- package/src/table/createTableColumnResizeState.ts +242 -0
- package/src/table/createTableState.d.ts +7 -4
- package/src/table/createTableState.ts +30 -7
- package/src/table/createTreeGridState.ts +146 -0
- package/src/table/index.d.ts +14 -4
- package/src/table/index.ts +13 -3
- package/src/table/types.d.ts +98 -92
- package/src/table/types.ts +83 -13
- package/src/tabs/createTabListState.d.ts +59 -57
- package/src/tabs/createTabListState.ts +34 -39
- package/src/tabs/index.d.ts +8 -2
- package/src/tabs/index.ts +1 -1
- package/src/textfield/createTextFieldState.d.ts +16 -14
- package/src/textfield/createTextFieldState.ts +7 -18
- package/src/textfield/index.d.ts +6 -2
- package/src/textfield/index.ts +1 -1
- package/src/toast/createToastState.d.ts +92 -86
- package/src/toast/createToastState.ts +90 -52
- package/src/toast/index.d.ts +12 -2
- package/src/toast/index.ts +2 -1
- package/src/toggle/createToggleGroupState.ts +11 -21
- package/src/toggle/createToggleState.d.ts +19 -19
- package/src/toggle/createToggleState.ts +2 -16
- package/src/toggle/index.d.ts +2 -2
- package/src/toggle/index.ts +2 -6
- package/src/tooltip/createTooltipTriggerState.d.ts +17 -17
- package/src/tooltip/createTooltipTriggerState.ts +12 -11
- package/src/tooltip/index.d.ts +7 -2
- package/src/tooltip/index.ts +1 -1
- package/src/tree/TreeCollection.d.ts +26 -23
- package/src/tree/TreeCollection.ts +29 -22
- package/src/tree/createTreeState.d.ts +7 -4
- package/src/tree/createTreeState.ts +70 -41
- package/src/tree/index.d.ts +10 -4
- package/src/tree/index.ts +3 -3
- package/src/tree/types.d.ts +131 -126
- package/src/tree/types.ts +30 -13
- package/src/utils/index.d.ts +2 -2
- package/src/utils/index.ts +1 -1
- package/src/utils/reactivity.d.ts +2 -2
- package/src/utils/reactivity.ts +3 -3
|
@@ -3,38 +3,51 @@
|
|
|
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
|
|
14
|
+
import { createSignal, createMemo, createEffect, type Accessor } from "solid-js";
|
|
9
15
|
import {
|
|
10
|
-
type
|
|
16
|
+
type AnyDateTime,
|
|
17
|
+
type Calendar,
|
|
11
18
|
type CalendarDateTime,
|
|
12
|
-
type ZonedDateTime,
|
|
13
19
|
type DateValue,
|
|
20
|
+
GregorianCalendar,
|
|
14
21
|
today,
|
|
15
22
|
getLocalTimeZone,
|
|
16
23
|
DateFormatter,
|
|
17
24
|
toCalendarDate as intlToCalendarDate,
|
|
18
25
|
toCalendarDateTime,
|
|
19
|
-
} from
|
|
20
|
-
import { access, type MaybeAccessor } from
|
|
21
|
-
import type { ValidationState } from
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
} from "@internationalized/date";
|
|
27
|
+
import { access, type MaybeAccessor } from "../utils";
|
|
28
|
+
import type { ValidationState } from "./createCalendarState";
|
|
29
|
+
import { IncompleteDate } from "./IncompleteDate";
|
|
30
|
+
import {
|
|
31
|
+
createFormValidationState,
|
|
32
|
+
DEFAULT_VALIDATION_RESULT,
|
|
33
|
+
VALID_VALIDITY_STATE,
|
|
34
|
+
type ValidationBehavior,
|
|
35
|
+
type ValidationFunction,
|
|
36
|
+
type ValidationResult,
|
|
37
|
+
type ValidityState,
|
|
38
|
+
} from "../form";
|
|
26
39
|
|
|
27
40
|
export type DateSegmentType =
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
41
|
+
| "year"
|
|
42
|
+
| "month"
|
|
43
|
+
| "day"
|
|
44
|
+
| "hour"
|
|
45
|
+
| "minute"
|
|
46
|
+
| "second"
|
|
47
|
+
| "dayPeriod"
|
|
48
|
+
| "era"
|
|
49
|
+
| "timeZoneName"
|
|
50
|
+
| "literal";
|
|
38
51
|
|
|
39
52
|
export interface DateSegment {
|
|
40
53
|
/** The type of segment. */
|
|
@@ -42,7 +55,7 @@ export interface DateSegment {
|
|
|
42
55
|
/** The text content of the segment. */
|
|
43
56
|
text: string;
|
|
44
57
|
/** The numeric value of the segment (if applicable). */
|
|
45
|
-
value?: number;
|
|
58
|
+
value?: number | null;
|
|
46
59
|
/** The minimum value for the segment. */
|
|
47
60
|
minValue?: number;
|
|
48
61
|
/** The maximum value for the segment. */
|
|
@@ -72,18 +85,26 @@ export interface DateFieldStateProps<T extends DateValue = DateValue> {
|
|
|
72
85
|
isReadOnly?: MaybeAccessor<boolean>;
|
|
73
86
|
/** Whether the field is required. */
|
|
74
87
|
isRequired?: MaybeAccessor<boolean>;
|
|
88
|
+
/** Whether the value is invalid (controlled). */
|
|
89
|
+
isInvalid?: boolean;
|
|
75
90
|
/** The locale to use for formatting. */
|
|
76
91
|
locale?: string;
|
|
77
92
|
/** The granularity of the date/time (day, hour, minute, second). */
|
|
78
|
-
granularity?:
|
|
93
|
+
granularity?: "day" | "hour" | "minute" | "second";
|
|
79
94
|
/** Whether to show the hour in 12 or 24 hour format. */
|
|
80
95
|
hourCycle?: 12 | 24;
|
|
81
96
|
/** Whether to hide the time zone. */
|
|
82
97
|
hideTimeZone?: boolean;
|
|
83
98
|
/** The placeholder date (determines segment structure). */
|
|
84
99
|
placeholderValue?: DateValue;
|
|
100
|
+
/** Whether to force leading zeroes in month/day segments. */
|
|
101
|
+
shouldForceLeadingZeros?: boolean;
|
|
85
102
|
/** Validation state. */
|
|
86
103
|
validationState?: MaybeAccessor<ValidationState | undefined>;
|
|
104
|
+
/** Validation behavior mode. */
|
|
105
|
+
validationBehavior?: ValidationBehavior;
|
|
106
|
+
/** Custom validation function. */
|
|
107
|
+
validate?: ValidationFunction<T>;
|
|
87
108
|
/** Description text. */
|
|
88
109
|
description?: string;
|
|
89
110
|
/** Error message. */
|
|
@@ -92,6 +113,8 @@ export interface DateFieldStateProps<T extends DateValue = DateValue> {
|
|
|
92
113
|
allowsNonContiguousRanges?: boolean;
|
|
93
114
|
/** Whether to create a date or datetime. */
|
|
94
115
|
createCalendar?: (name: string) => unknown;
|
|
116
|
+
/** Whether a date is unavailable. */
|
|
117
|
+
isDateUnavailable?: (date: DateValue) => boolean;
|
|
95
118
|
}
|
|
96
119
|
|
|
97
120
|
export interface DateFieldState<T extends DateValue = DateValue> {
|
|
@@ -124,294 +147,339 @@ export interface DateFieldState<T extends DateValue = DateValue> {
|
|
|
124
147
|
/** The validation state. */
|
|
125
148
|
validationState: Accessor<ValidationState | undefined>;
|
|
126
149
|
/** The granularity. */
|
|
127
|
-
granularity:
|
|
150
|
+
granularity: "day" | "hour" | "minute" | "second";
|
|
128
151
|
/** Whether the value is invalid. */
|
|
129
152
|
isInvalid: Accessor<boolean>;
|
|
153
|
+
/** Realtime validation results, including native and custom constraints. */
|
|
154
|
+
realtimeValidation: Accessor<ValidationResult>;
|
|
155
|
+
/** Currently displayed validation results. */
|
|
156
|
+
displayValidation: Accessor<ValidationResult>;
|
|
157
|
+
/** Updates the current validation result. */
|
|
158
|
+
updateValidation: (result: ValidationResult) => void;
|
|
159
|
+
/** Resets displayed validation to valid. */
|
|
160
|
+
resetValidation: () => void;
|
|
161
|
+
/** Commits realtime validation to displayed validation. */
|
|
162
|
+
commitValidation: () => void;
|
|
130
163
|
/** The locale. */
|
|
131
164
|
locale: string;
|
|
132
165
|
/** The time zone. */
|
|
133
166
|
timeZone: string;
|
|
167
|
+
/** The default value. */
|
|
168
|
+
defaultValue: T | null;
|
|
134
169
|
}
|
|
135
170
|
|
|
136
|
-
|
|
137
|
-
// IMPLEMENTATION
|
|
138
|
-
// ============================================
|
|
171
|
+
type HourCycle = "h11" | "h12" | "h23" | "h24";
|
|
139
172
|
|
|
140
173
|
/**
|
|
141
174
|
* Provides state management for a date field component.
|
|
142
175
|
*/
|
|
143
|
-
export function createDateFieldState<T extends DateValue =
|
|
144
|
-
props: DateFieldStateProps<T> = {}
|
|
176
|
+
export function createDateFieldState<T extends DateValue = DateValue>(
|
|
177
|
+
props: DateFieldStateProps<T> = {},
|
|
145
178
|
): DateFieldState<T> {
|
|
146
179
|
const timeZone = getLocalTimeZone();
|
|
147
|
-
const locale = props.locale ??
|
|
148
|
-
const granularity = props.granularity ??
|
|
180
|
+
const locale = props.locale ?? "en-US";
|
|
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
|
+
};
|
|
149
190
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
props.defaultValue ?? null
|
|
153
|
-
);
|
|
191
|
+
const toSource = (v: DateValue | null): Partial<Omit<AnyDateTime, "copy">> | null =>
|
|
192
|
+
(v as Partial<Omit<AnyDateTime, "copy">> | null) ?? null;
|
|
154
193
|
|
|
155
|
-
//
|
|
156
|
-
const
|
|
194
|
+
// State signals
|
|
195
|
+
const initialValue = props.defaultValue ?? null;
|
|
196
|
+
const [internalValue, setInternalValue] = createSignal<T | null>(initialValue);
|
|
157
197
|
|
|
158
|
-
// Controlled vs uncontrolled value
|
|
198
|
+
// Controlled vs uncontrolled committed value
|
|
159
199
|
const value = createMemo<T | null>(() => {
|
|
160
200
|
const controlled = access(props.value);
|
|
161
201
|
return controlled !== undefined ? controlled : internalValue();
|
|
162
202
|
});
|
|
163
203
|
|
|
164
|
-
// The
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// Build from placeholder parts
|
|
170
|
-
const parts = placeholderDate();
|
|
171
|
-
const placeholder = props.placeholderValue ?? today(timeZone);
|
|
172
|
-
|
|
173
|
-
if (Object.keys(parts).length === 0) return null;
|
|
174
|
-
|
|
175
|
-
// Create a date from the parts
|
|
176
|
-
const year = parts.year ?? placeholder.year;
|
|
177
|
-
const month = parts.month ?? placeholder.month;
|
|
178
|
-
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
|
+
);
|
|
179
208
|
|
|
180
|
-
|
|
181
|
-
|
|
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)));
|
|
182
217
|
}
|
|
183
|
-
|
|
184
|
-
// For time granularities
|
|
185
|
-
const hour = parts.hour ?? 0;
|
|
186
|
-
const minute = parts.minute ?? 0;
|
|
187
|
-
const second = parts.second ?? 0;
|
|
188
|
-
|
|
189
|
-
return toCalendarDateTime(placeholder).set({
|
|
190
|
-
year,
|
|
191
|
-
month,
|
|
192
|
-
day,
|
|
193
|
-
hour,
|
|
194
|
-
minute,
|
|
195
|
-
second,
|
|
196
|
-
}) as unknown as T;
|
|
197
218
|
});
|
|
198
219
|
|
|
199
220
|
// Derived states
|
|
200
221
|
const isDisabled = createMemo(() => access(props.isDisabled) ?? false);
|
|
201
222
|
const isReadOnly = createMemo(() => access(props.isReadOnly) ?? false);
|
|
202
223
|
const isRequired = createMemo(() => access(props.isRequired) ?? false);
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const isInvalid = createMemo(() => {
|
|
224
|
+
const explicitValidationState = createMemo(() => access(props.validationState));
|
|
225
|
+
const validationBehavior = () => props.validationBehavior ?? "native";
|
|
226
|
+
const builtinValidation = createMemo<ValidationResult>(() => {
|
|
207
227
|
const v = value();
|
|
208
|
-
if (!v) return
|
|
228
|
+
if (!v) return DEFAULT_VALIDATION_RESULT;
|
|
209
229
|
|
|
210
230
|
const minValue = access(props.minValue);
|
|
211
231
|
const maxValue = access(props.maxValue);
|
|
232
|
+
const validationDetails: ValidityState = { ...VALID_VALIDITY_STATE };
|
|
233
|
+
const validationErrors: string[] = [];
|
|
212
234
|
|
|
213
|
-
if (minValue && v.compare(minValue) < 0)
|
|
214
|
-
|
|
235
|
+
if (minValue && v.compare(minValue) < 0) {
|
|
236
|
+
validationDetails.rangeUnderflow = true;
|
|
237
|
+
validationErrors.push("Value is below the minimum date.");
|
|
238
|
+
}
|
|
215
239
|
|
|
216
|
-
|
|
217
|
-
|
|
240
|
+
if (maxValue && v.compare(maxValue) > 0) {
|
|
241
|
+
validationDetails.rangeOverflow = true;
|
|
242
|
+
validationErrors.push("Value is above the maximum date.");
|
|
243
|
+
}
|
|
218
244
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const parts = placeholderDate();
|
|
245
|
+
if (props.isDateUnavailable?.(v)) {
|
|
246
|
+
validationDetails.customError = true;
|
|
247
|
+
validationErrors.push("Date is unavailable.");
|
|
248
|
+
}
|
|
224
249
|
|
|
225
|
-
|
|
250
|
+
if (validationErrors.length === 0) {
|
|
251
|
+
return DEFAULT_VALIDATION_RESULT;
|
|
252
|
+
}
|
|
226
253
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
254
|
+
validationDetails.valid = false;
|
|
255
|
+
return {
|
|
256
|
+
isInvalid: true,
|
|
257
|
+
validationDetails,
|
|
258
|
+
validationErrors,
|
|
232
259
|
};
|
|
260
|
+
});
|
|
261
|
+
const validation = createFormValidationState<T>({
|
|
262
|
+
get value() {
|
|
263
|
+
return value();
|
|
264
|
+
},
|
|
265
|
+
get isInvalid() {
|
|
266
|
+
return props.isInvalid;
|
|
267
|
+
},
|
|
268
|
+
get validationState() {
|
|
269
|
+
return explicitValidationState();
|
|
270
|
+
},
|
|
271
|
+
get validationBehavior() {
|
|
272
|
+
return validationBehavior();
|
|
273
|
+
},
|
|
274
|
+
get validate() {
|
|
275
|
+
return props.validate;
|
|
276
|
+
},
|
|
277
|
+
get builtinValidation() {
|
|
278
|
+
return builtinValidation();
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
const isInvalid = createMemo(() => validation.displayValidation().isInvalid);
|
|
282
|
+
const validationState = createMemo<ValidationState | undefined>(
|
|
283
|
+
() => explicitValidationState() ?? (isInvalid() ? "invalid" : undefined),
|
|
284
|
+
);
|
|
233
285
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
286
|
+
// The effective date value, derived from the display override.
|
|
287
|
+
const dateValue = createMemo<DateValue | null>(() => {
|
|
288
|
+
const v = value();
|
|
289
|
+
if (v) return v;
|
|
290
|
+
const dv = displayValue();
|
|
291
|
+
if (dv.isCleared(displaySegments)) return null;
|
|
292
|
+
return dv.toValue(getPlaceholderValue());
|
|
293
|
+
});
|
|
294
|
+
|
|
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);
|
|
244
301
|
|
|
302
|
+
const shouldForceLeadingZeros = props.shouldForceLeadingZeros === true;
|
|
303
|
+
const formatOptions = buildFormatOptions(granularity, shouldForceLeadingZeros, props.hourCycle);
|
|
245
304
|
const formatter = new DateFormatter(locale, formatOptions);
|
|
246
|
-
const
|
|
247
|
-
const
|
|
305
|
+
const resolvedOptions = formatter.resolvedOptions() as unknown as Record<string, string>;
|
|
306
|
+
const parts = formatter.formatToParts(jsDate);
|
|
248
307
|
|
|
249
|
-
|
|
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) {
|
|
250
316
|
const type = mapPartType(part.type);
|
|
251
317
|
|
|
252
|
-
if (type ===
|
|
253
|
-
|
|
254
|
-
type:
|
|
318
|
+
if (type === "literal") {
|
|
319
|
+
result.push({
|
|
320
|
+
type: "literal",
|
|
255
321
|
text: part.value,
|
|
256
322
|
isEditable: false,
|
|
257
323
|
isPlaceholder: false,
|
|
258
324
|
placeholder: part.value,
|
|
259
325
|
});
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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);
|
|
276
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
|
+
});
|
|
277
365
|
}
|
|
278
366
|
|
|
279
|
-
return
|
|
367
|
+
return result;
|
|
280
368
|
});
|
|
281
369
|
|
|
282
|
-
//
|
|
283
|
-
const
|
|
284
|
-
if (isDisabled() || isReadOnly()) return;
|
|
285
|
-
|
|
370
|
+
// Commit a concrete value (updates the controlled/uncontrolled value and fires onChange).
|
|
371
|
+
const commitDate = (newValue: T | null) => {
|
|
286
372
|
const controlled = access(props.value);
|
|
287
373
|
if (controlled === undefined) {
|
|
374
|
+
lastSyncedValue = newValue;
|
|
288
375
|
setInternalValue(() => newValue);
|
|
289
376
|
}
|
|
290
|
-
|
|
291
|
-
if (props.onChange) {
|
|
292
|
-
props.onChange(newValue);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Clear placeholder parts
|
|
296
|
-
setPlaceholderDate({});
|
|
377
|
+
props.onChange?.(newValue);
|
|
297
378
|
};
|
|
298
379
|
|
|
299
|
-
// Set
|
|
300
|
-
|
|
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) => {
|
|
301
383
|
if (isDisabled() || isReadOnly()) return;
|
|
302
384
|
|
|
303
|
-
|
|
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;
|
|
392
|
+
}
|
|
304
393
|
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
} else {
|
|
310
|
-
// Update placeholder parts
|
|
311
|
-
setPlaceholderDate((prev) => ({
|
|
312
|
-
...prev,
|
|
313
|
-
[type]: newValue,
|
|
314
|
-
}));
|
|
394
|
+
if (!(newValue instanceof IncompleteDate)) {
|
|
395
|
+
commitDate(newValue);
|
|
396
|
+
setDisplayValue(new IncompleteDate(calendar, hourCycle, toSource(newValue)));
|
|
397
|
+
return;
|
|
315
398
|
}
|
|
316
|
-
};
|
|
317
399
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (
|
|
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
|
+
}
|
|
321
413
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
? getSegmentValue(v, type)
|
|
325
|
-
: placeholderDate()[type as keyof DateParts];
|
|
326
|
-
const max = getMaxValue(type, v ?? props.placeholderValue ?? today(timeZone));
|
|
327
|
-
const min = getMinValue(type);
|
|
328
|
-
|
|
329
|
-
const newValue = current !== undefined ? current + 1 : min;
|
|
330
|
-
setSegment(type, newValue > max ? min : newValue);
|
|
414
|
+
// Incomplete/invalid value. Set temporary display override.
|
|
415
|
+
setDisplayValue(newValue);
|
|
331
416
|
};
|
|
332
417
|
|
|
333
|
-
|
|
334
|
-
const decrementSegment = (type: DateSegmentType) => {
|
|
418
|
+
const adjustSegment = (type: DateSegmentType, amount: number) => {
|
|
335
419
|
if (isDisabled() || isReadOnly()) return;
|
|
420
|
+
setValue(displayValue().cycle(type, amount, getPlaceholderValue(), displaySegments));
|
|
421
|
+
};
|
|
336
422
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const max = getMaxValue(type, v ?? props.placeholderValue ?? today(timeZone));
|
|
342
|
-
const min = getMinValue(type);
|
|
343
|
-
|
|
344
|
-
const newValue = current !== undefined ? current - 1 : max;
|
|
345
|
-
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()));
|
|
346
427
|
};
|
|
347
428
|
|
|
348
|
-
|
|
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.
|
|
349
433
|
const clearSegment = (type: DateSegmentType) => {
|
|
350
434
|
if (isDisabled() || isReadOnly()) return;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// Clear entire value if any segment is cleared
|
|
355
|
-
setValue(null);
|
|
356
|
-
} else {
|
|
357
|
-
setPlaceholderDate((prev) => {
|
|
358
|
-
const next = { ...prev };
|
|
359
|
-
delete next[type as keyof DateParts];
|
|
360
|
-
return next;
|
|
361
|
-
});
|
|
435
|
+
let dv = displayValue();
|
|
436
|
+
if (type !== "timeZoneName" && type !== "literal") {
|
|
437
|
+
dv = dv.clear(type);
|
|
362
438
|
}
|
|
439
|
+
setValue(dv);
|
|
363
440
|
};
|
|
364
441
|
|
|
365
|
-
//
|
|
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.
|
|
366
445
|
const confirmPlaceholder = () => {
|
|
367
446
|
if (isDisabled() || isReadOnly()) return;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return maxValue;
|
|
377
|
-
}
|
|
378
|
-
return candidate;
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
const parts = placeholderDate();
|
|
382
|
-
if (Object.keys(parts).length > 0) {
|
|
383
|
-
const dv = dateValue();
|
|
384
|
-
if (dv) {
|
|
385
|
-
setValue(constrain(dv) as T);
|
|
386
|
-
}
|
|
387
|
-
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);
|
|
388
455
|
}
|
|
389
456
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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)));
|
|
396
464
|
}
|
|
397
465
|
};
|
|
398
466
|
|
|
399
467
|
// Format the current value
|
|
400
468
|
const formatValue = (fieldOptions?: Intl.DateTimeFormatOptions): string => {
|
|
401
469
|
const v = value();
|
|
402
|
-
if (!v) return
|
|
470
|
+
if (!v) return "";
|
|
403
471
|
|
|
404
472
|
const options: Intl.DateTimeFormatOptions = fieldOptions ?? {
|
|
405
|
-
year:
|
|
406
|
-
month:
|
|
407
|
-
day:
|
|
473
|
+
year: "numeric",
|
|
474
|
+
month: props.shouldForceLeadingZeros === true ? "2-digit" : "numeric",
|
|
475
|
+
day: props.shouldForceLeadingZeros === true ? "2-digit" : "numeric",
|
|
408
476
|
};
|
|
409
477
|
|
|
410
|
-
if (granularity !==
|
|
411
|
-
options.hour =
|
|
412
|
-
options.minute =
|
|
413
|
-
if (granularity ===
|
|
414
|
-
options.second =
|
|
478
|
+
if (granularity !== "day") {
|
|
479
|
+
options.hour = "2-digit";
|
|
480
|
+
options.minute = "2-digit";
|
|
481
|
+
if (granularity === "second") {
|
|
482
|
+
options.second = "2-digit";
|
|
415
483
|
}
|
|
416
484
|
}
|
|
417
485
|
|
|
@@ -421,6 +489,7 @@ export function createDateFieldState<T extends DateValue = CalendarDate>(
|
|
|
421
489
|
|
|
422
490
|
return {
|
|
423
491
|
value,
|
|
492
|
+
defaultValue: initialValue,
|
|
424
493
|
dateValue,
|
|
425
494
|
setValue,
|
|
426
495
|
segments,
|
|
@@ -436,150 +505,194 @@ export function createDateFieldState<T extends DateValue = CalendarDate>(
|
|
|
436
505
|
validationState,
|
|
437
506
|
granularity,
|
|
438
507
|
isInvalid,
|
|
508
|
+
realtimeValidation: validation.realtimeValidation,
|
|
509
|
+
displayValidation: validation.displayValidation,
|
|
510
|
+
updateValidation: validation.updateValidation,
|
|
511
|
+
resetValidation: validation.resetValidation,
|
|
512
|
+
commitValidation: validation.commitValidation,
|
|
439
513
|
locale,
|
|
440
514
|
timeZone,
|
|
441
515
|
};
|
|
442
516
|
}
|
|
443
517
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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;
|
|
455
531
|
}
|
|
456
532
|
|
|
457
|
-
function
|
|
533
|
+
function rawSegmentValue(dv: IncompleteDate, type: DateSegmentType): number | string | null {
|
|
458
534
|
switch (type) {
|
|
459
|
-
case
|
|
460
|
-
return
|
|
461
|
-
case
|
|
462
|
-
return
|
|
463
|
-
case
|
|
464
|
-
return
|
|
465
|
-
case
|
|
466
|
-
return
|
|
467
|
-
case
|
|
468
|
-
return
|
|
469
|
-
case
|
|
470
|
-
return
|
|
471
|
-
case
|
|
472
|
-
return
|
|
473
|
-
case
|
|
474
|
-
return
|
|
475
|
-
case 'timeZoneName':
|
|
476
|
-
return 'timeZoneName';
|
|
477
|
-
case 'literal':
|
|
478
|
-
return 'literal';
|
|
535
|
+
case "era":
|
|
536
|
+
return dv.era;
|
|
537
|
+
case "year":
|
|
538
|
+
return dv.year;
|
|
539
|
+
case "month":
|
|
540
|
+
return dv.month;
|
|
541
|
+
case "day":
|
|
542
|
+
return dv.day;
|
|
543
|
+
case "hour":
|
|
544
|
+
return dv.hour;
|
|
545
|
+
case "minute":
|
|
546
|
+
return dv.minute;
|
|
547
|
+
case "second":
|
|
548
|
+
return dv.second;
|
|
549
|
+
case "dayPeriod":
|
|
550
|
+
return dv.dayPeriod;
|
|
479
551
|
default:
|
|
480
552
|
return null;
|
|
481
553
|
}
|
|
482
554
|
}
|
|
483
555
|
|
|
484
|
-
function
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
+
}
|
|
489
563
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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);
|
|
506
585
|
}
|
|
507
586
|
|
|
508
|
-
function
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
+
}
|
|
524
607
|
}
|
|
608
|
+
|
|
609
|
+
return formatOptions;
|
|
525
610
|
}
|
|
526
611
|
|
|
527
|
-
function
|
|
612
|
+
function mapPartType(type: Intl.DateTimeFormatPartTypes): DateSegmentType | null {
|
|
528
613
|
switch (type) {
|
|
529
|
-
case
|
|
530
|
-
return
|
|
531
|
-
case
|
|
532
|
-
return
|
|
533
|
-
case
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
614
|
+
case "year":
|
|
615
|
+
return "year";
|
|
616
|
+
case "month":
|
|
617
|
+
return "month";
|
|
618
|
+
case "day":
|
|
619
|
+
return "day";
|
|
620
|
+
case "hour":
|
|
621
|
+
return "hour";
|
|
622
|
+
case "minute":
|
|
623
|
+
return "minute";
|
|
624
|
+
case "second":
|
|
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";
|
|
542
634
|
default:
|
|
543
|
-
return
|
|
635
|
+
return null;
|
|
544
636
|
}
|
|
545
637
|
}
|
|
546
638
|
|
|
547
|
-
function getPlaceholderText(
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
639
|
+
function getPlaceholderText(
|
|
640
|
+
type: DateSegmentType,
|
|
641
|
+
locale: string,
|
|
642
|
+
granularity: "day" | "hour" | "minute" | "second",
|
|
643
|
+
shouldForceLeadingZeros = false,
|
|
644
|
+
): string {
|
|
645
|
+
if (type === "literal") return "";
|
|
646
|
+
if (type === "dayPeriod") return "AM";
|
|
647
|
+
|
|
648
|
+
const formatOptions: Intl.DateTimeFormatOptions = {
|
|
649
|
+
year: "numeric",
|
|
650
|
+
month: shouldForceLeadingZeros ? "2-digit" : "numeric",
|
|
651
|
+
day: shouldForceLeadingZeros ? "2-digit" : "numeric",
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
if (granularity !== "day") {
|
|
655
|
+
formatOptions.hour = "2-digit";
|
|
656
|
+
formatOptions.minute = "2-digit";
|
|
657
|
+
if (granularity === "second") {
|
|
658
|
+
formatOptions.second = "2-digit";
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const formatter = new DateFormatter(locale, formatOptions);
|
|
663
|
+
// Use a reference date with recognizable digits
|
|
664
|
+
const refDate = new Date(2222, 10, 22, 22, 22, 22);
|
|
665
|
+
const parts = formatter.formatToParts(refDate);
|
|
666
|
+
|
|
667
|
+
for (const part of parts) {
|
|
668
|
+
const mappedType = mapPartType(part.type);
|
|
669
|
+
if (mappedType === type) {
|
|
670
|
+
return part.value.replace(/\d/g, (_d) => {
|
|
671
|
+
if (type === "year") return "y";
|
|
672
|
+
if (type === "month") return "m";
|
|
673
|
+
if (type === "day") return "d";
|
|
674
|
+
return "–";
|
|
675
|
+
});
|
|
676
|
+
}
|
|
565
677
|
}
|
|
566
|
-
}
|
|
567
678
|
|
|
568
|
-
function updateDatePart(date: DateValue, type: DateSegmentType, value: number): DateValue {
|
|
569
679
|
switch (type) {
|
|
570
|
-
case
|
|
571
|
-
return
|
|
572
|
-
case
|
|
573
|
-
return
|
|
574
|
-
case
|
|
575
|
-
return
|
|
576
|
-
case
|
|
577
|
-
return
|
|
578
|
-
case
|
|
579
|
-
return
|
|
580
|
-
case
|
|
581
|
-
return
|
|
680
|
+
case "year":
|
|
681
|
+
return "yyyy";
|
|
682
|
+
case "month":
|
|
683
|
+
return "mm";
|
|
684
|
+
case "day":
|
|
685
|
+
return "dd";
|
|
686
|
+
case "hour":
|
|
687
|
+
return "––";
|
|
688
|
+
case "minute":
|
|
689
|
+
return "––";
|
|
690
|
+
case "second":
|
|
691
|
+
return "––";
|
|
582
692
|
default:
|
|
583
|
-
return
|
|
693
|
+
return "";
|
|
584
694
|
}
|
|
585
695
|
}
|
|
696
|
+
|
|
697
|
+
// Re-export so existing imports of CalendarDateTime keep resolving through this module.
|
|
698
|
+
export type { CalendarDateTime };
|