@react-stately/datepicker 3.0.0-rc.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +460 -62
- package/dist/main.js.map +1 -1
- package/dist/module.js +460 -62
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +7 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/placeholders.ts +108 -0
- package/src/useDateFieldState.ts +66 -43
- package/src/useDatePickerState.ts +2 -1
- package/src/useDateRangePickerState.ts +7 -23
- package/src/utils.ts +6 -1
package/src/useDateFieldState.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import {Calendar, DateFormatter, getMinimumDayInMonth, getMinimumMonthInYear, GregorianCalendar, toCalendar} from '@internationalized/date';
|
|
14
14
|
import {convertValue, createPlaceholderDate, FieldOptions, getFormatOptions, isInvalid, useDefaultProps} from './utils';
|
|
15
15
|
import {DatePickerProps, DateValue, Granularity} from '@react-types/datepicker';
|
|
16
|
+
import {getPlaceholder} from './placeholders';
|
|
16
17
|
import {useControlledState} from '@react-stately/utils';
|
|
17
18
|
import {useEffect, useMemo, useRef, useState} from 'react';
|
|
18
19
|
import {ValidationState} from '@react-types/shared';
|
|
@@ -31,6 +32,8 @@ export interface DateSegment {
|
|
|
31
32
|
maxValue?: number,
|
|
32
33
|
/** Whether the value is a placeholder. */
|
|
33
34
|
isPlaceholder: boolean,
|
|
35
|
+
/** A placeholder string for the segment. */
|
|
36
|
+
placeholder: string,
|
|
34
37
|
/** Whether the segment is editable. */
|
|
35
38
|
isEditable: boolean
|
|
36
39
|
}
|
|
@@ -40,6 +43,8 @@ export interface DateFieldState {
|
|
|
40
43
|
value: DateValue,
|
|
41
44
|
/** The current value, converted to a native JavaScript `Date` object. */
|
|
42
45
|
dateValue: Date,
|
|
46
|
+
/** The calendar system currently in use. */
|
|
47
|
+
calendar: Calendar,
|
|
43
48
|
/** Sets the field's value. */
|
|
44
49
|
setValue(value: DateValue): void,
|
|
45
50
|
/** A list of segments for the current value. */
|
|
@@ -75,12 +80,10 @@ export interface DateFieldState {
|
|
|
75
80
|
*/
|
|
76
81
|
decrementPage(type: SegmentType): void,
|
|
77
82
|
/** Sets the value of the given segment. */
|
|
83
|
+
setSegment(type: 'era', value: string): void,
|
|
78
84
|
setSegment(type: SegmentType, value: number): void,
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
* If a segment type is provided, only that segment is confirmed. Otherwise, all segments that have not been entered yet are confirmed.
|
|
82
|
-
*/
|
|
83
|
-
confirmPlaceholder(type?: SegmentType): void,
|
|
85
|
+
/** Updates the remaining unfilled segments with the placeholder value. */
|
|
86
|
+
confirmPlaceholder(): void,
|
|
84
87
|
/** Clears the value of the given segment, reverting it to the placeholder. */
|
|
85
88
|
clearSegment(type: SegmentType): void,
|
|
86
89
|
/** Formats the current date value using the given options. */
|
|
@@ -153,22 +156,43 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
|
|
|
153
156
|
throw new Error('Invalid granularity ' + granularity + ' for value ' + v.toString());
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
let defaultFormatter = useMemo(() => new DateFormatter(locale), [locale]);
|
|
160
|
+
let calendar = useMemo(() => createCalendar(defaultFormatter.resolvedOptions().calendar), [createCalendar, defaultFormatter]);
|
|
161
|
+
|
|
162
|
+
let [value, setDate] = useControlledState<DateValue>(
|
|
163
|
+
props.value,
|
|
164
|
+
props.defaultValue,
|
|
165
|
+
props.onChange
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
let calendarValue = useMemo(() => convertValue(value, calendar), [value, calendar]);
|
|
169
|
+
|
|
170
|
+
// We keep track of the placeholder date separately in state so that onChange is not called
|
|
171
|
+
// until all segments are set. If the value === null (not undefined), then assume the component
|
|
172
|
+
// is controlled, so use the placeholder as the value until all segments are entered so it doesn't
|
|
173
|
+
// change from uncontrolled to controlled and emit a warning.
|
|
174
|
+
let [placeholderDate, setPlaceholderDate] = useState(
|
|
175
|
+
() => createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone)
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
let val = calendarValue || placeholderDate;
|
|
179
|
+
let showEra = calendar.identifier === 'gregory' && val.era === 'BC';
|
|
156
180
|
let formatOpts = useMemo(() => ({
|
|
157
181
|
granularity,
|
|
158
182
|
maxGranularity: props.maxGranularity ?? 'year',
|
|
159
183
|
timeZone: defaultTimeZone,
|
|
160
184
|
hideTimeZone,
|
|
161
|
-
hourCycle: props.hourCycle
|
|
162
|
-
|
|
185
|
+
hourCycle: props.hourCycle,
|
|
186
|
+
showEra
|
|
187
|
+
}), [props.maxGranularity, granularity, props.hourCycle, defaultTimeZone, hideTimeZone, showEra]);
|
|
163
188
|
let opts = useMemo(() => getFormatOptions({}, formatOpts), [formatOpts]);
|
|
164
189
|
|
|
165
190
|
let dateFormatter = useMemo(() => new DateFormatter(locale, opts), [locale, opts]);
|
|
166
191
|
let resolvedOptions = useMemo(() => dateFormatter.resolvedOptions(), [dateFormatter]);
|
|
167
|
-
let calendar = useMemo(() => createCalendar(resolvedOptions.calendar), [createCalendar, resolvedOptions.calendar]);
|
|
168
192
|
|
|
169
193
|
// Determine how many editable segments there are for validation purposes.
|
|
170
194
|
// The result is cached for performance.
|
|
171
|
-
let allSegments = useMemo(() =>
|
|
195
|
+
let allSegments: Partial<typeof EDITABLE_SEGMENTS> = useMemo(() =>
|
|
172
196
|
dateFormatter.formatToParts(new Date())
|
|
173
197
|
.filter(seg => EDITABLE_SEGMENTS[seg.type])
|
|
174
198
|
.reduce((p, seg) => (p[seg.type] = true, p), {})
|
|
@@ -178,14 +202,6 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
|
|
|
178
202
|
() => props.value || props.defaultValue ? {...allSegments} : {}
|
|
179
203
|
);
|
|
180
204
|
|
|
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
205
|
// Reset placeholder when calendar changes
|
|
190
206
|
let lastCalendarIdentifier = useRef(calendar.identifier);
|
|
191
207
|
useEffect(() => {
|
|
@@ -199,14 +215,6 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
|
|
|
199
215
|
}
|
|
200
216
|
}, [calendar, granularity, validSegments, defaultTimeZone, props.placeholderValue]);
|
|
201
217
|
|
|
202
|
-
let [value, setDate] = useControlledState<DateValue>(
|
|
203
|
-
props.value,
|
|
204
|
-
props.defaultValue,
|
|
205
|
-
props.onChange
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
let calendarValue = useMemo(() => convertValue(value, calendar), [value, calendar]);
|
|
209
|
-
|
|
210
218
|
// If there is a value prop, and some segments were previously placeholders, mark them all as valid.
|
|
211
219
|
if (value && Object.keys(validSegments).length < Object.keys(allSegments).length) {
|
|
212
220
|
validSegments = {...allSegments};
|
|
@@ -246,29 +254,46 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
|
|
|
246
254
|
isEditable = false;
|
|
247
255
|
}
|
|
248
256
|
|
|
257
|
+
let isPlaceholder = EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type];
|
|
258
|
+
let placeholder = EDITABLE_SEGMENTS[segment.type] ? getPlaceholder(segment.type, segment.value, locale) : null;
|
|
249
259
|
return {
|
|
250
260
|
type: TYPE_MAPPING[segment.type] || segment.type,
|
|
251
|
-
text: segment.value,
|
|
261
|
+
text: isPlaceholder ? placeholder : segment.value,
|
|
252
262
|
...getSegmentLimits(displayValue, segment.type, resolvedOptions),
|
|
253
|
-
isPlaceholder
|
|
263
|
+
isPlaceholder,
|
|
264
|
+
placeholder,
|
|
254
265
|
isEditable
|
|
255
266
|
} as DateSegment;
|
|
256
267
|
})
|
|
257
|
-
, [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar]);
|
|
268
|
+
, [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale]);
|
|
258
269
|
|
|
259
|
-
|
|
270
|
+
// When the era field appears, mark it valid if the year field is already valid.
|
|
271
|
+
// If the era field disappears, remove it from the valid segments.
|
|
272
|
+
if (allSegments.era && validSegments.year && !validSegments.era) {
|
|
273
|
+
validSegments.era = true;
|
|
274
|
+
setValidSegments({...validSegments});
|
|
275
|
+
} else if (!allSegments.era && validSegments.era) {
|
|
276
|
+
delete validSegments.era;
|
|
277
|
+
setValidSegments({...validSegments});
|
|
278
|
+
}
|
|
260
279
|
|
|
261
280
|
let markValid = (part: Intl.DateTimeFormatPartTypes) => {
|
|
262
281
|
validSegments[part] = true;
|
|
263
|
-
if (part === 'year' &&
|
|
282
|
+
if (part === 'year' && allSegments.era) {
|
|
264
283
|
validSegments.era = true;
|
|
265
284
|
}
|
|
266
285
|
setValidSegments({...validSegments});
|
|
267
286
|
};
|
|
268
287
|
|
|
269
288
|
let adjustSegment = (type: Intl.DateTimeFormatPartTypes, amount: number) => {
|
|
270
|
-
|
|
271
|
-
|
|
289
|
+
if (!validSegments[type]) {
|
|
290
|
+
markValid(type);
|
|
291
|
+
if (Object.keys(validSegments).length >= Object.keys(allSegments).length) {
|
|
292
|
+
setValue(displayValue);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
setValue(addSegment(displayValue, type, amount, resolvedOptions));
|
|
296
|
+
}
|
|
272
297
|
};
|
|
273
298
|
|
|
274
299
|
let validationState: ValidationState = props.validationState ||
|
|
@@ -277,6 +302,7 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
|
|
|
277
302
|
return {
|
|
278
303
|
value: calendarValue,
|
|
279
304
|
dateValue,
|
|
305
|
+
calendar,
|
|
280
306
|
setValue,
|
|
281
307
|
segments,
|
|
282
308
|
dateFormatter,
|
|
@@ -302,21 +328,17 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
|
|
|
302
328
|
markValid(part);
|
|
303
329
|
setValue(setSegment(displayValue, part, v, resolvedOptions));
|
|
304
330
|
},
|
|
305
|
-
confirmPlaceholder(
|
|
331
|
+
confirmPlaceholder() {
|
|
306
332
|
if (props.isDisabled || props.isReadOnly) {
|
|
307
333
|
return;
|
|
308
334
|
}
|
|
309
335
|
|
|
310
|
-
if
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
setValue(displayValue.copy());
|
|
317
|
-
}
|
|
318
|
-
} else if (!validSegments[part]) {
|
|
319
|
-
markValid(part);
|
|
336
|
+
// Confirm the placeholder if only the day period is not filled in.
|
|
337
|
+
let validKeys = Object.keys(validSegments);
|
|
338
|
+
let allKeys = Object.keys(allSegments);
|
|
339
|
+
if (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod) {
|
|
340
|
+
validSegments = {...allSegments};
|
|
341
|
+
setValidSegments(validSegments);
|
|
320
342
|
setValue(displayValue.copy());
|
|
321
343
|
}
|
|
322
344
|
},
|
|
@@ -458,6 +480,7 @@ function setSegment(value: DateValue, part: string, segmentValue: number, option
|
|
|
458
480
|
case 'day':
|
|
459
481
|
case 'month':
|
|
460
482
|
case 'year':
|
|
483
|
+
case 'era':
|
|
461
484
|
return value.set({[part]: segmentValue});
|
|
462
485
|
}
|
|
463
486
|
|
|
@@ -153,7 +153,8 @@ export function useDatePickerState(props: DatePickerStateOptions): DatePickerSta
|
|
|
153
153
|
granularity,
|
|
154
154
|
timeZone: defaultTimeZone,
|
|
155
155
|
hideTimeZone: props.hideTimeZone,
|
|
156
|
-
hourCycle: props.hourCycle
|
|
156
|
+
hourCycle: props.hourCycle,
|
|
157
|
+
showEra: value.calendar.identifier === 'gregory' && value.era === 'BC'
|
|
157
158
|
});
|
|
158
159
|
|
|
159
160
|
let formatter = new DateFormatter(locale, formatOptions);
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {createPlaceholderDate, FieldOptions, getFormatOptions, getPlaceholderTime, isInvalid, useDefaultProps} from './utils';
|
|
14
13
|
import {DateFormatter, toCalendarDate, toCalendarDateTime} from '@internationalized/date';
|
|
15
14
|
import {DateRange, DateRangePickerProps, DateValue, Granularity, TimeValue} from '@react-types/datepicker';
|
|
15
|
+
import {FieldOptions, getFormatOptions, getPlaceholderTime, isInvalid, useDefaultProps} from './utils';
|
|
16
16
|
import {RangeValue, ValidationState} from '@react-types/shared';
|
|
17
17
|
import {useControlledState} from '@react-stately/utils';
|
|
18
18
|
import {useOverlayTriggerState} from '@react-stately/overlays';
|
|
19
|
-
import {
|
|
19
|
+
import {useState} from 'react';
|
|
20
20
|
|
|
21
21
|
export interface DateRangePickerStateOptions extends DateRangePickerProps<DateValue> {
|
|
22
22
|
/**
|
|
@@ -63,9 +63,7 @@ export interface DateRangePickerState {
|
|
|
63
63
|
/** The current validation state of the date picker, based on the `validationState`, `minValue`, and `maxValue` props. */
|
|
64
64
|
validationState: ValidationState,
|
|
65
65
|
/** Formats the selected range using the given options. */
|
|
66
|
-
formatValue(locale: string, fieldOptions: FieldOptions): {start: string, end: string}
|
|
67
|
-
/** Replaces the start and/or end value of the selected range with the placeholder value if unentered. */
|
|
68
|
-
confirmPlaceholder(): void
|
|
66
|
+
formatValue(locale: string, fieldOptions: FieldOptions): {start: string, end: string}
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
/**
|
|
@@ -85,11 +83,8 @@ export function useDateRangePickerState(props: DateRangePickerStateOptions): Dat
|
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
let value = controlledValue || placeholderValue;
|
|
88
|
-
let valueRef = useRef(value);
|
|
89
|
-
valueRef.current = value;
|
|
90
86
|
|
|
91
87
|
let setValue = (value: DateRange) => {
|
|
92
|
-
valueRef.current = value;
|
|
93
88
|
setPlaceholderValue(value);
|
|
94
89
|
if (value?.start && value.end) {
|
|
95
90
|
setControlledValue(value);
|
|
@@ -99,7 +94,7 @@ export function useDateRangePickerState(props: DateRangePickerStateOptions): Dat
|
|
|
99
94
|
};
|
|
100
95
|
|
|
101
96
|
let v = (value?.start || value?.end || props.placeholderValue);
|
|
102
|
-
let [granularity
|
|
97
|
+
let [granularity] = useDefaultProps(v, props.granularity);
|
|
103
98
|
let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second' || granularity === 'millisecond';
|
|
104
99
|
let shouldCloseOnSelect = props.shouldCloseOnSelect ?? true;
|
|
105
100
|
|
|
@@ -207,7 +202,9 @@ export function useDateRangePickerState(props: DateRangePickerStateOptions): Dat
|
|
|
207
202
|
granularity: startGranularity,
|
|
208
203
|
timeZone: startTimeZone,
|
|
209
204
|
hideTimeZone: props.hideTimeZone,
|
|
210
|
-
hourCycle: props.hourCycle
|
|
205
|
+
hourCycle: props.hourCycle,
|
|
206
|
+
showEra: (value.start.calendar.identifier === 'gregory' && value.start.era === 'BC') ||
|
|
207
|
+
(value.end.calendar.identifier === 'gregory' && value.end.era === 'BC')
|
|
211
208
|
});
|
|
212
209
|
|
|
213
210
|
let startDate = value.start.toDate(startTimeZone || 'UTC');
|
|
@@ -266,19 +263,6 @@ export function useDateRangePickerState(props: DateRangePickerStateOptions): Dat
|
|
|
266
263
|
start: startFormatter.format(startDate),
|
|
267
264
|
end: endFormatter.format(endDate)
|
|
268
265
|
};
|
|
269
|
-
},
|
|
270
|
-
confirmPlaceholder() {
|
|
271
|
-
// Need to use ref value here because the value can be set in the same tick as
|
|
272
|
-
// a blur, which means the component won't have re-rendered yet.
|
|
273
|
-
let value = valueRef.current;
|
|
274
|
-
if (value && Boolean(value.start) !== Boolean(value.end)) {
|
|
275
|
-
let calendar = (value.start || value.end).calendar;
|
|
276
|
-
let placeholder = createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone);
|
|
277
|
-
setValue({
|
|
278
|
-
start: value.start || placeholder,
|
|
279
|
-
end: value.end || placeholder
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
266
|
}
|
|
283
267
|
};
|
|
284
268
|
}
|
package/src/utils.ts
CHANGED
|
@@ -27,7 +27,8 @@ interface FormatterOptions {
|
|
|
27
27
|
hideTimeZone?: boolean,
|
|
28
28
|
granularity?: DatePickerProps<any>['granularity'],
|
|
29
29
|
maxGranularity?: 'year' | 'month' | DatePickerProps<any>['granularity'],
|
|
30
|
-
hourCycle?: 12 | 24
|
|
30
|
+
hourCycle?: 12 | 24,
|
|
31
|
+
showEra?: boolean
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
const DEFAULT_FIELD_OPTIONS: FieldOptions = {
|
|
@@ -76,6 +77,10 @@ export function getFormatOptions(
|
|
|
76
77
|
opts.timeZoneName = 'short';
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
if (options.showEra && startIdx === 0) {
|
|
81
|
+
opts.era = 'short';
|
|
82
|
+
}
|
|
83
|
+
|
|
79
84
|
return opts;
|
|
80
85
|
}
|
|
81
86
|
|