@react-stately/datepicker 3.0.0-alpha.4 → 3.0.0-nightly-4980928d3-240906
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/ar-AE.main.js +9 -0
- package/dist/ar-AE.main.js.map +1 -0
- package/dist/ar-AE.mjs +11 -0
- package/dist/ar-AE.module.js +11 -0
- package/dist/ar-AE.module.js.map +1 -0
- package/dist/bg-BG.main.js +9 -0
- package/dist/bg-BG.main.js.map +1 -0
- package/dist/bg-BG.mjs +11 -0
- package/dist/bg-BG.module.js +11 -0
- package/dist/bg-BG.module.js.map +1 -0
- package/dist/cs-CZ.main.js +9 -0
- package/dist/cs-CZ.main.js.map +1 -0
- package/dist/cs-CZ.mjs +11 -0
- package/dist/cs-CZ.module.js +11 -0
- package/dist/cs-CZ.module.js.map +1 -0
- package/dist/da-DK.main.js +9 -0
- package/dist/da-DK.main.js.map +1 -0
- package/dist/da-DK.mjs +11 -0
- package/dist/da-DK.module.js +11 -0
- package/dist/da-DK.module.js.map +1 -0
- package/dist/de-DE.main.js +9 -0
- package/dist/de-DE.main.js.map +1 -0
- package/dist/de-DE.mjs +11 -0
- package/dist/de-DE.module.js +11 -0
- package/dist/de-DE.module.js.map +1 -0
- package/dist/el-GR.main.js +9 -0
- package/dist/el-GR.main.js.map +1 -0
- package/dist/el-GR.mjs +11 -0
- package/dist/el-GR.module.js +11 -0
- package/dist/el-GR.module.js.map +1 -0
- package/dist/en-US.main.js +9 -0
- package/dist/en-US.main.js.map +1 -0
- package/dist/en-US.mjs +11 -0
- package/dist/en-US.module.js +11 -0
- package/dist/en-US.module.js.map +1 -0
- package/dist/es-ES.main.js +9 -0
- package/dist/es-ES.main.js.map +1 -0
- package/dist/es-ES.mjs +11 -0
- package/dist/es-ES.module.js +11 -0
- package/dist/es-ES.module.js.map +1 -0
- package/dist/et-EE.main.js +9 -0
- package/dist/et-EE.main.js.map +1 -0
- package/dist/et-EE.mjs +11 -0
- package/dist/et-EE.module.js +11 -0
- package/dist/et-EE.module.js.map +1 -0
- package/dist/fi-FI.main.js +9 -0
- package/dist/fi-FI.main.js.map +1 -0
- package/dist/fi-FI.mjs +11 -0
- package/dist/fi-FI.module.js +11 -0
- package/dist/fi-FI.module.js.map +1 -0
- package/dist/fr-FR.main.js +9 -0
- package/dist/fr-FR.main.js.map +1 -0
- package/dist/fr-FR.mjs +11 -0
- package/dist/fr-FR.module.js +11 -0
- package/dist/fr-FR.module.js.map +1 -0
- package/dist/he-IL.main.js +9 -0
- package/dist/he-IL.main.js.map +1 -0
- package/dist/he-IL.mjs +11 -0
- package/dist/he-IL.module.js +11 -0
- package/dist/he-IL.module.js.map +1 -0
- package/dist/hr-HR.main.js +9 -0
- package/dist/hr-HR.main.js.map +1 -0
- package/dist/hr-HR.mjs +11 -0
- package/dist/hr-HR.module.js +11 -0
- package/dist/hr-HR.module.js.map +1 -0
- package/dist/hu-HU.main.js +9 -0
- package/dist/hu-HU.main.js.map +1 -0
- package/dist/hu-HU.mjs +11 -0
- package/dist/hu-HU.module.js +11 -0
- package/dist/hu-HU.module.js.map +1 -0
- package/dist/import.mjs +23 -0
- package/dist/intlStrings.main.js +108 -0
- package/dist/intlStrings.main.js.map +1 -0
- package/dist/intlStrings.mjs +110 -0
- package/dist/intlStrings.module.js +110 -0
- package/dist/intlStrings.module.js.map +1 -0
- package/dist/it-IT.main.js +9 -0
- package/dist/it-IT.main.js.map +1 -0
- package/dist/it-IT.mjs +11 -0
- package/dist/it-IT.module.js +11 -0
- package/dist/it-IT.module.js.map +1 -0
- package/dist/ja-JP.main.js +9 -0
- package/dist/ja-JP.main.js.map +1 -0
- package/dist/ja-JP.mjs +11 -0
- package/dist/ja-JP.module.js +11 -0
- package/dist/ja-JP.module.js.map +1 -0
- package/dist/ko-KR.main.js +9 -0
- package/dist/ko-KR.main.js.map +1 -0
- package/dist/ko-KR.mjs +11 -0
- package/dist/ko-KR.module.js +11 -0
- package/dist/ko-KR.module.js.map +1 -0
- package/dist/lt-LT.main.js +9 -0
- package/dist/lt-LT.main.js.map +1 -0
- package/dist/lt-LT.mjs +11 -0
- package/dist/lt-LT.module.js +11 -0
- package/dist/lt-LT.module.js.map +1 -0
- package/dist/lv-LV.main.js +9 -0
- package/dist/lv-LV.main.js.map +1 -0
- package/dist/lv-LV.mjs +11 -0
- package/dist/lv-LV.module.js +11 -0
- package/dist/lv-LV.module.js.map +1 -0
- package/dist/main.js +19 -765
- package/dist/main.js.map +1 -1
- package/dist/module.js +16 -751
- package/dist/module.js.map +1 -1
- package/dist/nb-NO.main.js +9 -0
- package/dist/nb-NO.main.js.map +1 -0
- package/dist/nb-NO.mjs +11 -0
- package/dist/nb-NO.module.js +11 -0
- package/dist/nb-NO.module.js.map +1 -0
- package/dist/nl-NL.main.js +9 -0
- package/dist/nl-NL.main.js.map +1 -0
- package/dist/nl-NL.mjs +11 -0
- package/dist/nl-NL.module.js +11 -0
- package/dist/nl-NL.module.js.map +1 -0
- package/dist/pl-PL.main.js +9 -0
- package/dist/pl-PL.main.js.map +1 -0
- package/dist/pl-PL.mjs +11 -0
- package/dist/pl-PL.module.js +11 -0
- package/dist/pl-PL.module.js.map +1 -0
- package/dist/placeholders.main.js +409 -0
- package/dist/placeholders.main.js.map +1 -0
- package/dist/placeholders.mjs +404 -0
- package/dist/placeholders.module.js +404 -0
- package/dist/placeholders.module.js.map +1 -0
- package/dist/pt-BR.main.js +9 -0
- package/dist/pt-BR.main.js.map +1 -0
- package/dist/pt-BR.mjs +11 -0
- package/dist/pt-BR.module.js +11 -0
- package/dist/pt-BR.module.js.map +1 -0
- package/dist/pt-PT.main.js +9 -0
- package/dist/pt-PT.main.js.map +1 -0
- package/dist/pt-PT.mjs +11 -0
- package/dist/pt-PT.module.js +11 -0
- package/dist/pt-PT.module.js.map +1 -0
- package/dist/ro-RO.main.js +9 -0
- package/dist/ro-RO.main.js.map +1 -0
- package/dist/ro-RO.mjs +11 -0
- package/dist/ro-RO.module.js +11 -0
- package/dist/ro-RO.module.js.map +1 -0
- package/dist/ru-RU.main.js +9 -0
- package/dist/ru-RU.main.js.map +1 -0
- package/dist/ru-RU.mjs +11 -0
- package/dist/ru-RU.module.js +11 -0
- package/dist/ru-RU.module.js.map +1 -0
- package/dist/sk-SK.main.js +9 -0
- package/dist/sk-SK.main.js.map +1 -0
- package/dist/sk-SK.mjs +11 -0
- package/dist/sk-SK.module.js +11 -0
- package/dist/sk-SK.module.js.map +1 -0
- package/dist/sl-SI.main.js +9 -0
- package/dist/sl-SI.main.js.map +1 -0
- package/dist/sl-SI.mjs +11 -0
- package/dist/sl-SI.module.js +11 -0
- package/dist/sl-SI.module.js.map +1 -0
- package/dist/sr-SP.main.js +9 -0
- package/dist/sr-SP.main.js.map +1 -0
- package/dist/sr-SP.mjs +11 -0
- package/dist/sr-SP.module.js +11 -0
- package/dist/sr-SP.module.js.map +1 -0
- package/dist/sv-SE.main.js +9 -0
- package/dist/sv-SE.main.js.map +1 -0
- package/dist/sv-SE.mjs +11 -0
- package/dist/sv-SE.module.js +11 -0
- package/dist/sv-SE.module.js.map +1 -0
- package/dist/tr-TR.main.js +9 -0
- package/dist/tr-TR.main.js.map +1 -0
- package/dist/tr-TR.mjs +11 -0
- package/dist/tr-TR.module.js +11 -0
- package/dist/tr-TR.module.js.map +1 -0
- package/dist/types.d.ts +70 -29
- package/dist/types.d.ts.map +1 -1
- package/dist/uk-UA.main.js +9 -0
- package/dist/uk-UA.main.js.map +1 -0
- package/dist/uk-UA.mjs +11 -0
- package/dist/uk-UA.module.js +11 -0
- package/dist/uk-UA.module.js.map +1 -0
- package/dist/useDateFieldState.main.js +449 -0
- package/dist/useDateFieldState.main.js.map +1 -0
- package/dist/useDateFieldState.mjs +444 -0
- package/dist/useDateFieldState.module.js +444 -0
- package/dist/useDateFieldState.module.js.map +1 -0
- package/dist/useDatePickerState.main.js +138 -0
- package/dist/useDatePickerState.main.js.map +1 -0
- package/dist/useDatePickerState.mjs +133 -0
- package/dist/useDatePickerState.module.js +133 -0
- package/dist/useDatePickerState.module.js.map +1 -0
- package/dist/useDateRangePickerState.main.js +245 -0
- package/dist/useDateRangePickerState.main.js.map +1 -0
- package/dist/useDateRangePickerState.mjs +240 -0
- package/dist/useDateRangePickerState.module.js +240 -0
- package/dist/useDateRangePickerState.module.js.map +1 -0
- package/dist/useTimeFieldState.main.js +86 -0
- package/dist/useTimeFieldState.main.js.map +1 -0
- package/dist/useTimeFieldState.mjs +81 -0
- package/dist/useTimeFieldState.module.js +81 -0
- package/dist/useTimeFieldState.module.js.map +1 -0
- package/dist/utils.main.js +190 -0
- package/dist/utils.main.js.map +1 -0
- package/dist/utils.mjs +179 -0
- package/dist/utils.module.js +179 -0
- package/dist/utils.module.js.map +1 -0
- package/dist/zh-CN.main.js +9 -0
- package/dist/zh-CN.main.js.map +1 -0
- package/dist/zh-CN.mjs +11 -0
- package/dist/zh-CN.module.js +11 -0
- package/dist/zh-CN.module.js.map +1 -0
- package/dist/zh-TW.main.js +9 -0
- package/dist/zh-TW.main.js.map +1 -0
- package/dist/zh-TW.mjs +11 -0
- package/dist/zh-TW.module.js +11 -0
- package/dist/zh-TW.module.js.map +1 -0
- package/package.json +17 -10
- package/src/index.ts +9 -4
- package/src/placeholders.ts +108 -0
- package/src/useDateFieldState.ts +124 -52
- package/src/useDatePickerState.ts +64 -27
- package/src/useDateRangePickerState.ts +103 -47
- package/src/useTimeFieldState.ts +27 -9
- package/src/utils.ts +140 -18
- package/LICENSE +0 -201
package/src/useDateFieldState.ts
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {Calendar, DateFormatter, getMinimumDayInMonth, getMinimumMonthInYear, GregorianCalendar, toCalendar} from '@internationalized/date';
|
|
14
|
-
import {convertValue, createPlaceholderDate, FieldOptions, getFormatOptions,
|
|
14
|
+
import {convertValue, createPlaceholderDate, FieldOptions, FormatterOptions, getFormatOptions, getValidationResult, useDefaultProps} from './utils';
|
|
15
15
|
import {DatePickerProps, DateValue, Granularity} from '@react-types/datepicker';
|
|
16
|
+
import {FormValidationState, useFormValidationState} from '@react-stately/form';
|
|
17
|
+
import {getPlaceholder} from './placeholders';
|
|
16
18
|
import {useControlledState} from '@react-stately/utils';
|
|
17
19
|
import {useEffect, useMemo, useRef, useState} from 'react';
|
|
18
20
|
import {ValidationState} from '@react-types/shared';
|
|
@@ -31,25 +33,36 @@ export interface DateSegment {
|
|
|
31
33
|
maxValue?: number,
|
|
32
34
|
/** Whether the value is a placeholder. */
|
|
33
35
|
isPlaceholder: boolean,
|
|
36
|
+
/** A placeholder string for the segment. */
|
|
37
|
+
placeholder: string,
|
|
34
38
|
/** Whether the segment is editable. */
|
|
35
39
|
isEditable: boolean
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
export interface DateFieldState {
|
|
42
|
+
export interface DateFieldState extends FormValidationState {
|
|
39
43
|
/** The current field value. */
|
|
40
44
|
value: DateValue,
|
|
41
45
|
/** The current value, converted to a native JavaScript `Date` object. */
|
|
42
46
|
dateValue: Date,
|
|
47
|
+
/** The calendar system currently in use. */
|
|
48
|
+
calendar: Calendar,
|
|
43
49
|
/** Sets the field's value. */
|
|
44
50
|
setValue(value: DateValue): void,
|
|
45
51
|
/** A list of segments for the current value. */
|
|
46
52
|
segments: DateSegment[],
|
|
47
53
|
/** A date formatter configured for the current locale and format. */
|
|
48
54
|
dateFormatter: DateFormatter,
|
|
49
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* The current validation state of the date field, based on the `validationState`, `minValue`, and `maxValue` props.
|
|
57
|
+
* @deprecated Use `isInvalid` instead.
|
|
58
|
+
*/
|
|
50
59
|
validationState: ValidationState,
|
|
60
|
+
/** Whether the date field is invalid, based on the `isInvalid`, `minValue`, and `maxValue` props. */
|
|
61
|
+
isInvalid: boolean,
|
|
51
62
|
/** The granularity for the field, based on the `granularity` prop and current value. */
|
|
52
63
|
granularity: Granularity,
|
|
64
|
+
/** The maximum date or time unit that is displayed in the field. */
|
|
65
|
+
maxGranularity: 'year' | 'month' | Granularity,
|
|
53
66
|
/** Whether the field is disabled. */
|
|
54
67
|
isDisabled: boolean,
|
|
55
68
|
/** Whether the field is read only. */
|
|
@@ -73,16 +86,16 @@ export interface DateFieldState {
|
|
|
73
86
|
*/
|
|
74
87
|
decrementPage(type: SegmentType): void,
|
|
75
88
|
/** Sets the value of the given segment. */
|
|
89
|
+
setSegment(type: 'era', value: string): void,
|
|
76
90
|
setSegment(type: SegmentType, value: number): void,
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
* If a segment type is provided, only that segment is confirmed. Otherwise, all segments that have not been entered yet are confirmed.
|
|
80
|
-
*/
|
|
81
|
-
confirmPlaceholder(type?: SegmentType): void,
|
|
91
|
+
/** Updates the remaining unfilled segments with the placeholder value. */
|
|
92
|
+
confirmPlaceholder(): void,
|
|
82
93
|
/** Clears the value of the given segment, reverting it to the placeholder. */
|
|
83
94
|
clearSegment(type: SegmentType): void,
|
|
84
95
|
/** Formats the current date value using the given options. */
|
|
85
|
-
formatValue(fieldOptions: FieldOptions): string
|
|
96
|
+
formatValue(fieldOptions: FieldOptions): string,
|
|
97
|
+
/** Gets a formatter based on state's props. */
|
|
98
|
+
getDateFormatter(locale: string, formatOptions: FormatterOptions): DateFormatter
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
const EDITABLE_SEGMENTS = {
|
|
@@ -110,7 +123,7 @@ const TYPE_MAPPING = {
|
|
|
110
123
|
dayperiod: 'dayPeriod'
|
|
111
124
|
};
|
|
112
125
|
|
|
113
|
-
interface
|
|
126
|
+
export interface DateFieldStateOptions<T extends DateValue = DateValue> extends DatePickerProps<T> {
|
|
114
127
|
/**
|
|
115
128
|
* The maximum unit to display in the date field.
|
|
116
129
|
* @default 'year'
|
|
@@ -132,14 +145,17 @@ interface DatePickerFieldProps extends DatePickerProps<DateValue> {
|
|
|
132
145
|
* A date field allows users to enter and edit date and time values using a keyboard.
|
|
133
146
|
* Each part of a date value is displayed in an individually editable segment.
|
|
134
147
|
*/
|
|
135
|
-
export function useDateFieldState(props:
|
|
148
|
+
export function useDateFieldState<T extends DateValue = DateValue>(props: DateFieldStateOptions<T>): DateFieldState {
|
|
136
149
|
let {
|
|
137
150
|
locale,
|
|
138
151
|
createCalendar,
|
|
139
152
|
hideTimeZone,
|
|
140
153
|
isDisabled,
|
|
141
154
|
isReadOnly,
|
|
142
|
-
isRequired
|
|
155
|
+
isRequired,
|
|
156
|
+
minValue,
|
|
157
|
+
maxValue,
|
|
158
|
+
isDateUnavailable
|
|
143
159
|
} = props;
|
|
144
160
|
|
|
145
161
|
let v: DateValue = (props.value || props.defaultValue || props.placeholderValue);
|
|
@@ -151,22 +167,44 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
151
167
|
throw new Error('Invalid granularity ' + granularity + ' for value ' + v.toString());
|
|
152
168
|
}
|
|
153
169
|
|
|
170
|
+
let defaultFormatter = useMemo(() => new DateFormatter(locale), [locale]);
|
|
171
|
+
let calendar = useMemo(() => createCalendar(defaultFormatter.resolvedOptions().calendar), [createCalendar, defaultFormatter]);
|
|
172
|
+
|
|
173
|
+
let [value, setDate] = useControlledState<DateValue>(
|
|
174
|
+
props.value,
|
|
175
|
+
props.defaultValue,
|
|
176
|
+
props.onChange
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
let calendarValue = useMemo(() => convertValue(value, calendar), [value, calendar]);
|
|
180
|
+
|
|
181
|
+
// We keep track of the placeholder date separately in state so that onChange is not called
|
|
182
|
+
// until all segments are set. If the value === null (not undefined), then assume the component
|
|
183
|
+
// is controlled, so use the placeholder as the value until all segments are entered so it doesn't
|
|
184
|
+
// change from uncontrolled to controlled and emit a warning.
|
|
185
|
+
let [placeholderDate, setPlaceholderDate] = useState(
|
|
186
|
+
() => createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
let val = calendarValue || placeholderDate;
|
|
190
|
+
let showEra = calendar.identifier === 'gregory' && val.era === 'BC';
|
|
154
191
|
let formatOpts = useMemo(() => ({
|
|
155
192
|
granularity,
|
|
156
193
|
maxGranularity: props.maxGranularity ?? 'year',
|
|
157
194
|
timeZone: defaultTimeZone,
|
|
158
195
|
hideTimeZone,
|
|
159
|
-
hourCycle: props.hourCycle
|
|
160
|
-
|
|
196
|
+
hourCycle: props.hourCycle,
|
|
197
|
+
showEra,
|
|
198
|
+
shouldForceLeadingZeros: props.shouldForceLeadingZeros
|
|
199
|
+
}), [props.maxGranularity, granularity, props.hourCycle, props.shouldForceLeadingZeros, defaultTimeZone, hideTimeZone, showEra]);
|
|
161
200
|
let opts = useMemo(() => getFormatOptions({}, formatOpts), [formatOpts]);
|
|
162
201
|
|
|
163
202
|
let dateFormatter = useMemo(() => new DateFormatter(locale, opts), [locale, opts]);
|
|
164
203
|
let resolvedOptions = useMemo(() => dateFormatter.resolvedOptions(), [dateFormatter]);
|
|
165
|
-
let calendar = useMemo(() => createCalendar(resolvedOptions.calendar), [createCalendar, resolvedOptions.calendar]);
|
|
166
204
|
|
|
167
205
|
// Determine how many editable segments there are for validation purposes.
|
|
168
206
|
// The result is cached for performance.
|
|
169
|
-
let allSegments = useMemo(() =>
|
|
207
|
+
let allSegments: Partial<typeof EDITABLE_SEGMENTS> = useMemo(() =>
|
|
170
208
|
dateFormatter.formatToParts(new Date())
|
|
171
209
|
.filter(seg => EDITABLE_SEGMENTS[seg.type])
|
|
172
210
|
.reduce((p, seg) => (p[seg.type] = true, p), {})
|
|
@@ -176,13 +214,7 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
176
214
|
() => props.value || props.defaultValue ? {...allSegments} : {}
|
|
177
215
|
);
|
|
178
216
|
|
|
179
|
-
|
|
180
|
-
// until all segments are set. If the value === null (not undefined), then assume the component
|
|
181
|
-
// is controlled, so use the placeholder as the value until all segments are entered so it doesn't
|
|
182
|
-
// change from uncontrolled to controlled and emit a warning.
|
|
183
|
-
let [placeholderDate, setPlaceholderDate] = useState(
|
|
184
|
-
() => createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone)
|
|
185
|
-
);
|
|
217
|
+
let clearedSegment = useRef<string>(undefined);
|
|
186
218
|
|
|
187
219
|
// Reset placeholder when calendar changes
|
|
188
220
|
let lastCalendarIdentifier = useRef(calendar.identifier);
|
|
@@ -197,14 +229,6 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
197
229
|
}
|
|
198
230
|
}, [calendar, granularity, validSegments, defaultTimeZone, props.placeholderValue]);
|
|
199
231
|
|
|
200
|
-
let [value, setDate] = useControlledState<DateValue>(
|
|
201
|
-
props.value,
|
|
202
|
-
props.defaultValue,
|
|
203
|
-
props.onChange
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
let calendarValue = useMemo(() => convertValue(value, calendar), [value, calendar]);
|
|
207
|
-
|
|
208
232
|
// If there is a value prop, and some segments were previously placeholders, mark them all as valid.
|
|
209
233
|
if (value && Object.keys(validSegments).length < Object.keys(allSegments).length) {
|
|
210
234
|
validSegments = {...allSegments};
|
|
@@ -224,8 +248,15 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
224
248
|
if (props.isDisabled || props.isReadOnly) {
|
|
225
249
|
return;
|
|
226
250
|
}
|
|
251
|
+
let validKeys = Object.keys(validSegments);
|
|
252
|
+
let allKeys = Object.keys(allSegments);
|
|
227
253
|
|
|
228
|
-
if
|
|
254
|
+
// if all the segments are completed or a timefield with everything but am/pm set the time, also ignore when am/pm cleared
|
|
255
|
+
if (newValue == null) {
|
|
256
|
+
setDate(null);
|
|
257
|
+
setPlaceholderDate(createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone));
|
|
258
|
+
setValidSegments({});
|
|
259
|
+
} else if (validKeys.length >= allKeys.length || (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod && clearedSegment.current !== 'dayPeriod')) {
|
|
229
260
|
// The display calendar should not have any effect on the emitted value.
|
|
230
261
|
// Emit dates in the same calendar as the original value, if any, otherwise gregorian.
|
|
231
262
|
newValue = toCalendar(newValue, v?.calendar || new GregorianCalendar());
|
|
@@ -233,6 +264,7 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
233
264
|
} else {
|
|
234
265
|
setPlaceholderDate(newValue);
|
|
235
266
|
}
|
|
267
|
+
clearedSegment.current = null;
|
|
236
268
|
};
|
|
237
269
|
|
|
238
270
|
let dateValue = useMemo(() => displayValue.toDate(timeZone), [displayValue, timeZone]);
|
|
@@ -244,42 +276,79 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
244
276
|
isEditable = false;
|
|
245
277
|
}
|
|
246
278
|
|
|
279
|
+
let isPlaceholder = EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type];
|
|
280
|
+
let placeholder = EDITABLE_SEGMENTS[segment.type] ? getPlaceholder(segment.type, segment.value, locale) : null;
|
|
247
281
|
return {
|
|
248
282
|
type: TYPE_MAPPING[segment.type] || segment.type,
|
|
249
|
-
text: segment.value,
|
|
283
|
+
text: isPlaceholder ? placeholder : segment.value,
|
|
250
284
|
...getSegmentLimits(displayValue, segment.type, resolvedOptions),
|
|
251
|
-
isPlaceholder
|
|
285
|
+
isPlaceholder,
|
|
286
|
+
placeholder,
|
|
252
287
|
isEditable
|
|
253
288
|
} as DateSegment;
|
|
254
289
|
})
|
|
255
|
-
, [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar]);
|
|
290
|
+
, [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale]);
|
|
256
291
|
|
|
257
|
-
|
|
292
|
+
// When the era field appears, mark it valid if the year field is already valid.
|
|
293
|
+
// If the era field disappears, remove it from the valid segments.
|
|
294
|
+
if (allSegments.era && validSegments.year && !validSegments.era) {
|
|
295
|
+
validSegments.era = true;
|
|
296
|
+
setValidSegments({...validSegments});
|
|
297
|
+
} else if (!allSegments.era && validSegments.era) {
|
|
298
|
+
delete validSegments.era;
|
|
299
|
+
setValidSegments({...validSegments});
|
|
300
|
+
}
|
|
258
301
|
|
|
259
302
|
let markValid = (part: Intl.DateTimeFormatPartTypes) => {
|
|
260
303
|
validSegments[part] = true;
|
|
261
|
-
if (part === 'year' &&
|
|
304
|
+
if (part === 'year' && allSegments.era) {
|
|
262
305
|
validSegments.era = true;
|
|
263
306
|
}
|
|
264
307
|
setValidSegments({...validSegments});
|
|
265
308
|
};
|
|
266
309
|
|
|
267
310
|
let adjustSegment = (type: Intl.DateTimeFormatPartTypes, amount: number) => {
|
|
268
|
-
|
|
269
|
-
|
|
311
|
+
if (!validSegments[type]) {
|
|
312
|
+
markValid(type);
|
|
313
|
+
let validKeys = Object.keys(validSegments);
|
|
314
|
+
let allKeys = Object.keys(allSegments);
|
|
315
|
+
if (validKeys.length >= allKeys.length || (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod)) {
|
|
316
|
+
setValue(displayValue);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
setValue(addSegment(displayValue, type, amount, resolvedOptions));
|
|
320
|
+
}
|
|
270
321
|
};
|
|
271
322
|
|
|
272
|
-
let
|
|
273
|
-
|
|
323
|
+
let builtinValidation = useMemo(() => getValidationResult(
|
|
324
|
+
value,
|
|
325
|
+
minValue,
|
|
326
|
+
maxValue,
|
|
327
|
+
isDateUnavailable,
|
|
328
|
+
formatOpts
|
|
329
|
+
), [value, minValue, maxValue, isDateUnavailable, formatOpts]);
|
|
330
|
+
|
|
331
|
+
let validation = useFormValidationState({
|
|
332
|
+
...props,
|
|
333
|
+
value,
|
|
334
|
+
builtinValidation
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
let isValueInvalid = validation.displayValidation.isInvalid;
|
|
338
|
+
let validationState: ValidationState = props.validationState || (isValueInvalid ? 'invalid' : null);
|
|
274
339
|
|
|
275
340
|
return {
|
|
341
|
+
...validation,
|
|
276
342
|
value: calendarValue,
|
|
277
343
|
dateValue,
|
|
344
|
+
calendar,
|
|
278
345
|
setValue,
|
|
279
346
|
segments,
|
|
280
347
|
dateFormatter,
|
|
281
348
|
validationState,
|
|
349
|
+
isInvalid: isValueInvalid,
|
|
282
350
|
granularity,
|
|
351
|
+
maxGranularity: props.maxGranularity ?? 'year',
|
|
283
352
|
isDisabled,
|
|
284
353
|
isReadOnly,
|
|
285
354
|
isRequired,
|
|
@@ -299,26 +368,23 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
299
368
|
markValid(part);
|
|
300
369
|
setValue(setSegment(displayValue, part, v, resolvedOptions));
|
|
301
370
|
},
|
|
302
|
-
confirmPlaceholder(
|
|
371
|
+
confirmPlaceholder() {
|
|
303
372
|
if (props.isDisabled || props.isReadOnly) {
|
|
304
373
|
return;
|
|
305
374
|
}
|
|
306
375
|
|
|
307
|
-
if
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
setValue(displayValue.copy());
|
|
314
|
-
}
|
|
315
|
-
} else if (!validSegments[part]) {
|
|
316
|
-
markValid(part);
|
|
376
|
+
// Confirm the placeholder if only the day period is not filled in.
|
|
377
|
+
let validKeys = Object.keys(validSegments);
|
|
378
|
+
let allKeys = Object.keys(allSegments);
|
|
379
|
+
if (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod) {
|
|
380
|
+
validSegments = {...allSegments};
|
|
381
|
+
setValidSegments(validSegments);
|
|
317
382
|
setValue(displayValue.copy());
|
|
318
383
|
}
|
|
319
384
|
},
|
|
320
385
|
clearSegment(part) {
|
|
321
386
|
delete validSegments[part];
|
|
387
|
+
clearedSegment.current = part;
|
|
322
388
|
setValidSegments({...validSegments});
|
|
323
389
|
|
|
324
390
|
let placeholder = createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone);
|
|
@@ -348,6 +414,11 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
348
414
|
let formatOptions = getFormatOptions(fieldOptions, formatOpts);
|
|
349
415
|
let formatter = new DateFormatter(locale, formatOptions);
|
|
350
416
|
return formatter.format(dateValue);
|
|
417
|
+
},
|
|
418
|
+
getDateFormatter(locale, formatOptions: FormatterOptions) {
|
|
419
|
+
let newOptions = {...formatOpts, ...formatOptions};
|
|
420
|
+
let newFormatOptions = getFormatOptions({}, newOptions);
|
|
421
|
+
return new DateFormatter(locale, newFormatOptions);
|
|
351
422
|
}
|
|
352
423
|
};
|
|
353
424
|
}
|
|
@@ -455,6 +526,7 @@ function setSegment(value: DateValue, part: string, segmentValue: number, option
|
|
|
455
526
|
case 'day':
|
|
456
527
|
case 'month':
|
|
457
528
|
case 'year':
|
|
529
|
+
case 'era':
|
|
458
530
|
return value.set({[part]: segmentValue});
|
|
459
531
|
}
|
|
460
532
|
|
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
import {CalendarDate, DateFormatter, toCalendarDate, toCalendarDateTime} from '@internationalized/date';
|
|
14
14
|
import {DatePickerProps, DateValue, Granularity, TimeValue} from '@react-types/datepicker';
|
|
15
|
-
import {FieldOptions, getFormatOptions, getPlaceholderTime, useDefaultProps} from './utils';
|
|
16
|
-
import {
|
|
15
|
+
import {FieldOptions, FormatterOptions, getFormatOptions, getPlaceholderTime, getValidationResult, useDefaultProps} from './utils';
|
|
16
|
+
import {FormValidationState, useFormValidationState} from '@react-stately/form';
|
|
17
|
+
import {OverlayTriggerState, useOverlayTriggerState} from '@react-stately/overlays';
|
|
17
18
|
import {useControlledState} from '@react-stately/utils';
|
|
18
|
-
import {useState} from 'react';
|
|
19
|
+
import {useMemo, useState} from 'react';
|
|
19
20
|
import {ValidationState} from '@react-types/shared';
|
|
20
21
|
|
|
21
|
-
export interface
|
|
22
|
+
export interface DatePickerStateOptions<T extends DateValue> extends DatePickerProps<T> {
|
|
22
23
|
/**
|
|
23
24
|
* Determines whether the date picker popover should close automatically when a date is selected.
|
|
24
25
|
* @default true
|
|
@@ -26,11 +27,11 @@ export interface DatePickerOptions extends DatePickerProps<DateValue> {
|
|
|
26
27
|
shouldCloseOnSelect?: boolean | (() => boolean)
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export interface DatePickerState {
|
|
30
|
+
export interface DatePickerState extends OverlayTriggerState, FormValidationState {
|
|
30
31
|
/** The currently selected date. */
|
|
31
|
-
value: DateValue,
|
|
32
|
+
value: DateValue | null,
|
|
32
33
|
/** Sets the selected date. */
|
|
33
|
-
setValue(value: DateValue): void,
|
|
34
|
+
setValue(value: DateValue | null): void,
|
|
34
35
|
/**
|
|
35
36
|
* The date portion of the value. This may be set prior to `value` if the user has
|
|
36
37
|
* selected a date but has not yet selected a time.
|
|
@@ -53,24 +54,31 @@ export interface DatePickerState {
|
|
|
53
54
|
isOpen: boolean,
|
|
54
55
|
/** Sets whether the calendar popover is open. */
|
|
55
56
|
setOpen(isOpen: boolean): void,
|
|
56
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The current validation state of the date picker, based on the `validationState`, `minValue`, and `maxValue` props.
|
|
59
|
+
* @deprecated Use `isInvalid` instead.
|
|
60
|
+
*/
|
|
57
61
|
validationState: ValidationState,
|
|
62
|
+
/** Whether the date picker is invalid, based on the `isInvalid`, `minValue`, and `maxValue` props. */
|
|
63
|
+
isInvalid: boolean,
|
|
58
64
|
/** Formats the selected value using the given options. */
|
|
59
|
-
formatValue(locale: string, fieldOptions: FieldOptions): string
|
|
65
|
+
formatValue(locale: string, fieldOptions: FieldOptions): string,
|
|
66
|
+
/** Gets a formatter based on state's props. */
|
|
67
|
+
getDateFormatter(locale: string, formatOptions: FormatterOptions): DateFormatter
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
/**
|
|
63
71
|
* Provides state management for a date picker component.
|
|
64
72
|
* A date picker combines a DateField and a Calendar popover to allow users to enter or select a date and time value.
|
|
65
73
|
*/
|
|
66
|
-
export function useDatePickerState(props:
|
|
67
|
-
let
|
|
74
|
+
export function useDatePickerState<T extends DateValue = DateValue>(props: DatePickerStateOptions<T>): DatePickerState {
|
|
75
|
+
let overlayState = useOverlayTriggerState(props);
|
|
68
76
|
let [value, setValue] = useControlledState<DateValue>(props.value, props.defaultValue || null, props.onChange);
|
|
69
77
|
|
|
70
78
|
let v = (value || props.placeholderValue);
|
|
71
79
|
let [granularity, defaultTimeZone] = useDefaultProps(v, props.granularity);
|
|
72
80
|
let dateValue = value != null ? value.toDate(defaultTimeZone ?? 'UTC') : null;
|
|
73
|
-
let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second'
|
|
81
|
+
let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second';
|
|
74
82
|
let shouldCloseOnSelect = props.shouldCloseOnSelect ?? true;
|
|
75
83
|
|
|
76
84
|
let [selectedDate, setSelectedDate] = useState<DateValue>(null);
|
|
@@ -88,8 +96,39 @@ export function useDatePickerState(props: DatePickerOptions): DatePickerState {
|
|
|
88
96
|
throw new Error('Invalid granularity ' + granularity + ' for value ' + v.toString());
|
|
89
97
|
}
|
|
90
98
|
|
|
99
|
+
let showEra = value?.calendar.identifier === 'gregory' && value.era === 'BC';
|
|
100
|
+
let formatOpts = useMemo(() => ({
|
|
101
|
+
granularity,
|
|
102
|
+
timeZone: defaultTimeZone,
|
|
103
|
+
hideTimeZone: props.hideTimeZone,
|
|
104
|
+
hourCycle: props.hourCycle,
|
|
105
|
+
shouldForceLeadingZeros: props.shouldForceLeadingZeros,
|
|
106
|
+
showEra
|
|
107
|
+
}), [granularity, props.hourCycle, props.shouldForceLeadingZeros, defaultTimeZone, props.hideTimeZone, showEra]);
|
|
108
|
+
|
|
109
|
+
let {minValue, maxValue, isDateUnavailable} = props;
|
|
110
|
+
let builtinValidation = useMemo(() => getValidationResult(
|
|
111
|
+
value,
|
|
112
|
+
minValue,
|
|
113
|
+
maxValue,
|
|
114
|
+
isDateUnavailable,
|
|
115
|
+
formatOpts
|
|
116
|
+
), [value, minValue, maxValue, isDateUnavailable, formatOpts]);
|
|
117
|
+
|
|
118
|
+
let validation = useFormValidationState({
|
|
119
|
+
...props,
|
|
120
|
+
value,
|
|
121
|
+
builtinValidation
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let isValueInvalid = validation.displayValidation.isInvalid;
|
|
125
|
+
let validationState: ValidationState = props.validationState || (isValueInvalid ? 'invalid' : null);
|
|
126
|
+
|
|
91
127
|
let commitValue = (date: DateValue, time: TimeValue) => {
|
|
92
128
|
setValue('timeZone' in time ? time.set(toCalendarDate(date)) : toCalendarDateTime(date, time));
|
|
129
|
+
setSelectedDate(null);
|
|
130
|
+
setSelectedTime(null);
|
|
131
|
+
validation.commitValidation();
|
|
93
132
|
};
|
|
94
133
|
|
|
95
134
|
// Intercept setValue to make sure the Time section is not changed by date selection in Calendar
|
|
@@ -103,26 +142,24 @@ export function useDatePickerState(props: DatePickerOptions): DatePickerState {
|
|
|
103
142
|
}
|
|
104
143
|
} else {
|
|
105
144
|
setValue(newValue);
|
|
145
|
+
validation.commitValidation();
|
|
106
146
|
}
|
|
107
147
|
|
|
108
148
|
if (shouldClose) {
|
|
109
|
-
setOpen(false);
|
|
149
|
+
overlayState.setOpen(false);
|
|
110
150
|
}
|
|
111
151
|
};
|
|
112
152
|
|
|
113
153
|
let selectTime = (newValue: TimeValue) => {
|
|
114
|
-
if (selectedDate) {
|
|
154
|
+
if (selectedDate && newValue) {
|
|
115
155
|
commitValue(selectedDate, newValue);
|
|
116
156
|
} else {
|
|
117
157
|
setSelectedTime(newValue);
|
|
118
158
|
}
|
|
119
159
|
};
|
|
120
160
|
|
|
121
|
-
let validationState: ValidationState = props.validationState ||
|
|
122
|
-
(isInvalid(value, props.minValue, props.maxValue) ? 'invalid' : null) ||
|
|
123
|
-
(value && props.isDateUnavailable?.(value) ? 'invalid' : null);
|
|
124
|
-
|
|
125
161
|
return {
|
|
162
|
+
...validation,
|
|
126
163
|
value,
|
|
127
164
|
setValue,
|
|
128
165
|
dateValue: selectedDate,
|
|
@@ -131,7 +168,7 @@ export function useDatePickerState(props: DatePickerOptions): DatePickerState {
|
|
|
131
168
|
setTimeValue: selectTime,
|
|
132
169
|
granularity,
|
|
133
170
|
hasTime,
|
|
134
|
-
|
|
171
|
+
...overlayState,
|
|
135
172
|
setOpen(isOpen) {
|
|
136
173
|
// Commit the selected date when the calendar is closed. Use a placeholder time if one wasn't set.
|
|
137
174
|
// If only the time was set and not the date, don't commit. The state will be preserved until
|
|
@@ -140,23 +177,23 @@ export function useDatePickerState(props: DatePickerOptions): DatePickerState {
|
|
|
140
177
|
commitValue(selectedDate, selectedTime || getPlaceholderTime(props.placeholderValue));
|
|
141
178
|
}
|
|
142
179
|
|
|
143
|
-
setOpen(isOpen);
|
|
180
|
+
overlayState.setOpen(isOpen);
|
|
144
181
|
},
|
|
145
182
|
validationState,
|
|
183
|
+
isInvalid: isValueInvalid,
|
|
146
184
|
formatValue(locale, fieldOptions) {
|
|
147
185
|
if (!dateValue) {
|
|
148
186
|
return '';
|
|
149
187
|
}
|
|
150
188
|
|
|
151
|
-
let formatOptions = getFormatOptions(fieldOptions,
|
|
152
|
-
granularity,
|
|
153
|
-
timeZone: defaultTimeZone,
|
|
154
|
-
hideTimeZone: props.hideTimeZone,
|
|
155
|
-
hourCycle: props.hourCycle
|
|
156
|
-
});
|
|
157
|
-
|
|
189
|
+
let formatOptions = getFormatOptions(fieldOptions, formatOpts);
|
|
158
190
|
let formatter = new DateFormatter(locale, formatOptions);
|
|
159
191
|
return formatter.format(dateValue);
|
|
192
|
+
},
|
|
193
|
+
getDateFormatter(locale, formatOptions: FormatterOptions) {
|
|
194
|
+
let newOptions = {...formatOpts, ...formatOptions};
|
|
195
|
+
let newFormatOptions = getFormatOptions({}, newOptions);
|
|
196
|
+
return new DateFormatter(locale, newFormatOptions);
|
|
160
197
|
}
|
|
161
198
|
};
|
|
162
199
|
}
|