@react-stately/datepicker 3.0.0-nightly.3180 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +503 -103
- package/dist/main.js.map +1 -1
- package/dist/module.js +499 -87
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +21 -16
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/index.ts +9 -4
- package/src/placeholders.ts +108 -0
- package/src/useDateFieldState.ts +71 -45
- package/src/useDatePickerState.ts +9 -7
- package/src/useDateRangePickerState.ts +49 -33
- package/src/useTimeFieldState.ts +2 -2
- package/src/utils.ts +6 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {MessageDictionary} from '@internationalized/message';
|
|
14
|
+
|
|
15
|
+
// These placeholders are based on the strings used by the <input type="date">
|
|
16
|
+
// implementations in Chrome and Firefox. Additional languages are supported
|
|
17
|
+
// here than React Spectrum's typical translations.
|
|
18
|
+
const placeholders = new MessageDictionary({
|
|
19
|
+
ach: {year: 'mwaka', month: 'dwe', day: 'nino'},
|
|
20
|
+
af: {year: 'jjjj', month: 'mm', day: 'dd'},
|
|
21
|
+
am: {year: 'ዓዓዓዓ', month: 'ሚሜ', day: 'ቀቀ'},
|
|
22
|
+
an: {year: 'aaaa', month: 'mm', day: 'dd'},
|
|
23
|
+
ar: {year: 'سنة', month: 'شهر', day: 'يوم'},
|
|
24
|
+
ast: {year: 'aaaa', month: 'mm', day: 'dd'},
|
|
25
|
+
az: {year: 'iiii', month: 'aa', day: 'gg'},
|
|
26
|
+
be: {year: 'гггг', month: 'мм', day: 'дд'},
|
|
27
|
+
bg: {year: 'гггг', month: 'мм', day: 'дд'},
|
|
28
|
+
bn: {year: 'yyyy', month: 'মিমি', day: 'dd'},
|
|
29
|
+
br: {year: 'bbbb', month: 'mm', day: 'dd'},
|
|
30
|
+
bs: {year: 'gggg', month: 'mm', day: 'dd'},
|
|
31
|
+
ca: {year: 'aaaa', month: 'mm', day: 'dd'},
|
|
32
|
+
cak: {year: 'jjjj', month: 'ii', day: "q'q'"},
|
|
33
|
+
ckb: {year: 'ساڵ', month: 'مانگ', day: 'ڕۆژ'},
|
|
34
|
+
cs: {year: 'rrrr', month: 'mm', day: 'dd'},
|
|
35
|
+
cy: {year: 'bbbb', month: 'mm', day: 'dd'},
|
|
36
|
+
da: {year: 'åååå', month: 'mm', day: 'dd'},
|
|
37
|
+
de: {year: 'jjjj', month: 'mm', day: 'tt'},
|
|
38
|
+
dsb: {year: 'llll', month: 'mm', day: 'źź'},
|
|
39
|
+
el: {year: 'εεεε', month: 'μμ', day: 'ηη'},
|
|
40
|
+
en: {year: 'yyyy', month: 'mm', day: 'dd'},
|
|
41
|
+
eo: {year: 'jjjj', month: 'mm', day: 'tt'},
|
|
42
|
+
es: {year: 'aaaa', month: 'mm', day: 'dd'},
|
|
43
|
+
et: {year: 'aaaa', month: 'kk', day: 'pp'},
|
|
44
|
+
eu: {year: 'uuuu', month: 'hh', day: 'ee'},
|
|
45
|
+
fa: {year: 'سال', month: 'ماه', day: 'روز'},
|
|
46
|
+
ff: {year: 'hhhh', month: 'll', day: 'ññ'},
|
|
47
|
+
fi: {year: 'vvvv', month: 'kk', day: 'pp'},
|
|
48
|
+
fr: {year: 'aaaa', month: 'mm', day: 'jj'},
|
|
49
|
+
fy: {year: 'jjjj', month: 'mm', day: 'dd'},
|
|
50
|
+
ga: {year: 'bbbb', month: 'mm', day: 'll'},
|
|
51
|
+
gd: {year: 'bbbb', month: 'mm', day: 'll'},
|
|
52
|
+
gl: {year: 'aaaa', month: 'mm', day: 'dd'},
|
|
53
|
+
he: {year: 'שנה', month: 'חודש', day: 'יום'},
|
|
54
|
+
hr: {year: 'gggg', month: 'mm', day: 'dd'},
|
|
55
|
+
hsb: {year: 'llll', month: 'mm', day: 'dd'},
|
|
56
|
+
hu: {year: 'éééé', month: 'hh', day: 'nn'},
|
|
57
|
+
ia: {year: 'aaaa', month: 'mm', day: 'dd'},
|
|
58
|
+
id: {year: 'tttt', month: 'bb', day: 'hh'},
|
|
59
|
+
it: {year: 'aaaa', month: 'mm', day: 'gg'},
|
|
60
|
+
ja: {year: ' 年 ', month: '月', day: '日'},
|
|
61
|
+
ka: {year: 'წწწწ', month: 'თთ', day: 'რრ'},
|
|
62
|
+
kk: {year: 'жжжж', month: 'аа', day: 'кк'},
|
|
63
|
+
kn: {year: 'ವವವವ', month: 'ಮಿಮೀ', day: 'ದಿದಿ'},
|
|
64
|
+
ko: {year: '연도', month: '월', day: '일'},
|
|
65
|
+
lb: {year: 'jjjj', month: 'mm', day: 'dd'},
|
|
66
|
+
lo: {year: 'ປປປປ', month: 'ດດ', day: 'ວວ'},
|
|
67
|
+
lt: {year: 'mmmm', month: 'mm', day: 'dd'},
|
|
68
|
+
lv: {year: 'gggg', month: 'mm', day: 'dd'},
|
|
69
|
+
meh: {year: 'aaaa', month: 'mm', day: 'dd'},
|
|
70
|
+
ml: {year: 'വർഷം', month: 'മാസം', day: 'തീയതി'},
|
|
71
|
+
ms: {year: 'tttt', month: 'mm', day: 'hh'},
|
|
72
|
+
nl: {year: 'jjjj', month: 'mm', day: 'dd'},
|
|
73
|
+
nn: {year: 'åååå', month: 'mm', day: 'dd'},
|
|
74
|
+
no: {year: 'åååå', month: 'mm', day: 'dd'},
|
|
75
|
+
oc: {year: 'aaaa', month: 'mm', day: 'jj'},
|
|
76
|
+
pl: {year: 'rrrr', month: 'mm', day: 'dd'},
|
|
77
|
+
pt: {year: 'aaaa', month: 'mm', day: 'dd'},
|
|
78
|
+
rm: {year: 'oooo', month: 'mm', day: 'dd'},
|
|
79
|
+
ro: {year: 'aaaa', month: 'll', day: 'zz'},
|
|
80
|
+
ru: {year: 'гггг', month: 'мм', day: 'дд'},
|
|
81
|
+
sc: {year: 'aaaa', month: 'mm', day: 'dd'},
|
|
82
|
+
scn: {year: 'aaaa', month: 'mm', day: 'jj'},
|
|
83
|
+
sk: {year: 'rrrr', month: 'mm', day: 'dd'},
|
|
84
|
+
sl: {year: 'llll', month: 'mm', day: 'dd'},
|
|
85
|
+
sr: {year: 'гггг', month: 'мм', day: 'дд'},
|
|
86
|
+
sv: {year: 'åååå', month: 'mm', day: 'dd'},
|
|
87
|
+
szl: {year: 'rrrr', month: 'mm', day: 'dd'},
|
|
88
|
+
tg: {year: 'сссс', month: 'мм', day: 'рр'},
|
|
89
|
+
th: {year: 'ปปปป', month: 'ดด', day: 'วว'},
|
|
90
|
+
tr: {year: 'yyyy', month: 'aa', day: 'gg'},
|
|
91
|
+
uk: {year: 'рррр', month: 'мм', day: 'дд'},
|
|
92
|
+
'zh-CN': {year: '年', month: '月', day: '日'},
|
|
93
|
+
'zh-TW': {year: '年', month: '月', day: '日'}
|
|
94
|
+
}, 'en');
|
|
95
|
+
|
|
96
|
+
export function getPlaceholder(field: string, value: string, locale: string) {
|
|
97
|
+
// Use the actual placeholder value for the era and day period fields.
|
|
98
|
+
if (field === 'era' || field === 'dayPeriod') {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (field === 'year' || field === 'month' || field === 'day') {
|
|
103
|
+
return placeholders.getStringForLocale(field, locale);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// For time fields (e.g. hour, minute, etc.), use two dashes as the placeholder.
|
|
107
|
+
return '––';
|
|
108
|
+
}
|
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. */
|
|
@@ -50,6 +55,8 @@ export interface DateFieldState {
|
|
|
50
55
|
validationState: ValidationState,
|
|
51
56
|
/** The granularity for the field, based on the `granularity` prop and current value. */
|
|
52
57
|
granularity: Granularity,
|
|
58
|
+
/** The maximum date or time unit that is displayed in the field. */
|
|
59
|
+
maxGranularity: 'year' | 'month' | Granularity,
|
|
53
60
|
/** Whether the field is disabled. */
|
|
54
61
|
isDisabled: boolean,
|
|
55
62
|
/** Whether the field is read only. */
|
|
@@ -73,12 +80,10 @@ export interface DateFieldState {
|
|
|
73
80
|
*/
|
|
74
81
|
decrementPage(type: SegmentType): void,
|
|
75
82
|
/** Sets the value of the given segment. */
|
|
83
|
+
setSegment(type: 'era', value: string): void,
|
|
76
84
|
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,
|
|
85
|
+
/** Updates the remaining unfilled segments with the placeholder value. */
|
|
86
|
+
confirmPlaceholder(): void,
|
|
82
87
|
/** Clears the value of the given segment, reverting it to the placeholder. */
|
|
83
88
|
clearSegment(type: SegmentType): void,
|
|
84
89
|
/** Formats the current date value using the given options. */
|
|
@@ -110,7 +115,7 @@ const TYPE_MAPPING = {
|
|
|
110
115
|
dayperiod: 'dayPeriod'
|
|
111
116
|
};
|
|
112
117
|
|
|
113
|
-
interface
|
|
118
|
+
export interface DateFieldStateOptions extends DatePickerProps<DateValue> {
|
|
114
119
|
/**
|
|
115
120
|
* The maximum unit to display in the date field.
|
|
116
121
|
* @default 'year'
|
|
@@ -132,7 +137,7 @@ interface DatePickerFieldProps extends DatePickerProps<DateValue> {
|
|
|
132
137
|
* A date field allows users to enter and edit date and time values using a keyboard.
|
|
133
138
|
* Each part of a date value is displayed in an individually editable segment.
|
|
134
139
|
*/
|
|
135
|
-
export function useDateFieldState(props:
|
|
140
|
+
export function useDateFieldState(props: DateFieldStateOptions): DateFieldState {
|
|
136
141
|
let {
|
|
137
142
|
locale,
|
|
138
143
|
createCalendar,
|
|
@@ -151,22 +156,43 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
151
156
|
throw new Error('Invalid granularity ' + granularity + ' for value ' + v.toString());
|
|
152
157
|
}
|
|
153
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';
|
|
154
180
|
let formatOpts = useMemo(() => ({
|
|
155
181
|
granularity,
|
|
156
182
|
maxGranularity: props.maxGranularity ?? 'year',
|
|
157
183
|
timeZone: defaultTimeZone,
|
|
158
184
|
hideTimeZone,
|
|
159
|
-
hourCycle: props.hourCycle
|
|
160
|
-
|
|
185
|
+
hourCycle: props.hourCycle,
|
|
186
|
+
showEra
|
|
187
|
+
}), [props.maxGranularity, granularity, props.hourCycle, defaultTimeZone, hideTimeZone, showEra]);
|
|
161
188
|
let opts = useMemo(() => getFormatOptions({}, formatOpts), [formatOpts]);
|
|
162
189
|
|
|
163
190
|
let dateFormatter = useMemo(() => new DateFormatter(locale, opts), [locale, opts]);
|
|
164
191
|
let resolvedOptions = useMemo(() => dateFormatter.resolvedOptions(), [dateFormatter]);
|
|
165
|
-
let calendar = useMemo(() => createCalendar(resolvedOptions.calendar), [createCalendar, resolvedOptions.calendar]);
|
|
166
192
|
|
|
167
193
|
// Determine how many editable segments there are for validation purposes.
|
|
168
194
|
// The result is cached for performance.
|
|
169
|
-
let allSegments = useMemo(() =>
|
|
195
|
+
let allSegments: Partial<typeof EDITABLE_SEGMENTS> = useMemo(() =>
|
|
170
196
|
dateFormatter.formatToParts(new Date())
|
|
171
197
|
.filter(seg => EDITABLE_SEGMENTS[seg.type])
|
|
172
198
|
.reduce((p, seg) => (p[seg.type] = true, p), {})
|
|
@@ -176,14 +202,6 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
176
202
|
() => props.value || props.defaultValue ? {...allSegments} : {}
|
|
177
203
|
);
|
|
178
204
|
|
|
179
|
-
// We keep track of the placeholder date separately in state so that onChange is not called
|
|
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
|
-
);
|
|
186
|
-
|
|
187
205
|
// Reset placeholder when calendar changes
|
|
188
206
|
let lastCalendarIdentifier = useRef(calendar.identifier);
|
|
189
207
|
useEffect(() => {
|
|
@@ -197,14 +215,6 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
197
215
|
}
|
|
198
216
|
}, [calendar, granularity, validSegments, defaultTimeZone, props.placeholderValue]);
|
|
199
217
|
|
|
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
218
|
// If there is a value prop, and some segments were previously placeholders, mark them all as valid.
|
|
209
219
|
if (value && Object.keys(validSegments).length < Object.keys(allSegments).length) {
|
|
210
220
|
validSegments = {...allSegments};
|
|
@@ -244,29 +254,46 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
244
254
|
isEditable = false;
|
|
245
255
|
}
|
|
246
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;
|
|
247
259
|
return {
|
|
248
260
|
type: TYPE_MAPPING[segment.type] || segment.type,
|
|
249
|
-
text: segment.value,
|
|
261
|
+
text: isPlaceholder ? placeholder : segment.value,
|
|
250
262
|
...getSegmentLimits(displayValue, segment.type, resolvedOptions),
|
|
251
|
-
isPlaceholder
|
|
263
|
+
isPlaceholder,
|
|
264
|
+
placeholder,
|
|
252
265
|
isEditable
|
|
253
266
|
} as DateSegment;
|
|
254
267
|
})
|
|
255
|
-
, [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar]);
|
|
268
|
+
, [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale]);
|
|
256
269
|
|
|
257
|
-
|
|
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
|
+
}
|
|
258
279
|
|
|
259
280
|
let markValid = (part: Intl.DateTimeFormatPartTypes) => {
|
|
260
281
|
validSegments[part] = true;
|
|
261
|
-
if (part === 'year' &&
|
|
282
|
+
if (part === 'year' && allSegments.era) {
|
|
262
283
|
validSegments.era = true;
|
|
263
284
|
}
|
|
264
285
|
setValidSegments({...validSegments});
|
|
265
286
|
};
|
|
266
287
|
|
|
267
288
|
let adjustSegment = (type: Intl.DateTimeFormatPartTypes, amount: number) => {
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
}
|
|
270
297
|
};
|
|
271
298
|
|
|
272
299
|
let validationState: ValidationState = props.validationState ||
|
|
@@ -275,11 +302,13 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
275
302
|
return {
|
|
276
303
|
value: calendarValue,
|
|
277
304
|
dateValue,
|
|
305
|
+
calendar,
|
|
278
306
|
setValue,
|
|
279
307
|
segments,
|
|
280
308
|
dateFormatter,
|
|
281
309
|
validationState,
|
|
282
310
|
granularity,
|
|
311
|
+
maxGranularity: props.maxGranularity ?? 'year',
|
|
283
312
|
isDisabled,
|
|
284
313
|
isReadOnly,
|
|
285
314
|
isRequired,
|
|
@@ -299,21 +328,17 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
|
|
|
299
328
|
markValid(part);
|
|
300
329
|
setValue(setSegment(displayValue, part, v, resolvedOptions));
|
|
301
330
|
},
|
|
302
|
-
confirmPlaceholder(
|
|
331
|
+
confirmPlaceholder() {
|
|
303
332
|
if (props.isDisabled || props.isReadOnly) {
|
|
304
333
|
return;
|
|
305
334
|
}
|
|
306
335
|
|
|
307
|
-
if
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
setValue(displayValue.copy());
|
|
314
|
-
}
|
|
315
|
-
} else if (!validSegments[part]) {
|
|
316
|
-
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);
|
|
317
342
|
setValue(displayValue.copy());
|
|
318
343
|
}
|
|
319
344
|
},
|
|
@@ -455,6 +480,7 @@ function setSegment(value: DateValue, part: string, segmentValue: number, option
|
|
|
455
480
|
case 'day':
|
|
456
481
|
case 'month':
|
|
457
482
|
case 'year':
|
|
483
|
+
case 'era':
|
|
458
484
|
return value.set({[part]: segmentValue});
|
|
459
485
|
}
|
|
460
486
|
|
|
@@ -15,10 +15,11 @@ import {DatePickerProps, DateValue, Granularity, TimeValue} from '@react-types/d
|
|
|
15
15
|
import {FieldOptions, getFormatOptions, getPlaceholderTime, useDefaultProps} from './utils';
|
|
16
16
|
import {isInvalid} from './utils';
|
|
17
17
|
import {useControlledState} from '@react-stately/utils';
|
|
18
|
+
import {useOverlayTriggerState} from '@react-stately/overlays';
|
|
18
19
|
import {useState} from 'react';
|
|
19
20
|
import {ValidationState} from '@react-types/shared';
|
|
20
21
|
|
|
21
|
-
export interface
|
|
22
|
+
export interface DatePickerStateOptions extends DatePickerProps<DateValue> {
|
|
22
23
|
/**
|
|
23
24
|
* Determines whether the date picker popover should close automatically when a date is selected.
|
|
24
25
|
* @default true
|
|
@@ -63,8 +64,8 @@ export interface DatePickerState {
|
|
|
63
64
|
* Provides state management for a date picker component.
|
|
64
65
|
* A date picker combines a DateField and a Calendar popover to allow users to enter or select a date and time value.
|
|
65
66
|
*/
|
|
66
|
-
export function useDatePickerState(props:
|
|
67
|
-
let
|
|
67
|
+
export function useDatePickerState(props: DatePickerStateOptions): DatePickerState {
|
|
68
|
+
let overlayState = useOverlayTriggerState(props);
|
|
68
69
|
let [value, setValue] = useControlledState<DateValue>(props.value, props.defaultValue || null, props.onChange);
|
|
69
70
|
|
|
70
71
|
let v = (value || props.placeholderValue);
|
|
@@ -106,7 +107,7 @@ export function useDatePickerState(props: DatePickerOptions): DatePickerState {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
if (shouldClose) {
|
|
109
|
-
setOpen(false);
|
|
110
|
+
overlayState.setOpen(false);
|
|
110
111
|
}
|
|
111
112
|
};
|
|
112
113
|
|
|
@@ -131,7 +132,7 @@ export function useDatePickerState(props: DatePickerOptions): DatePickerState {
|
|
|
131
132
|
setTimeValue: selectTime,
|
|
132
133
|
granularity,
|
|
133
134
|
hasTime,
|
|
134
|
-
isOpen,
|
|
135
|
+
isOpen: overlayState.isOpen,
|
|
135
136
|
setOpen(isOpen) {
|
|
136
137
|
// Commit the selected date when the calendar is closed. Use a placeholder time if one wasn't set.
|
|
137
138
|
// If only the time was set and not the date, don't commit. The state will be preserved until
|
|
@@ -140,7 +141,7 @@ export function useDatePickerState(props: DatePickerOptions): DatePickerState {
|
|
|
140
141
|
commitValue(selectedDate, selectedTime || getPlaceholderTime(props.placeholderValue));
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
setOpen(isOpen);
|
|
144
|
+
overlayState.setOpen(isOpen);
|
|
144
145
|
},
|
|
145
146
|
validationState,
|
|
146
147
|
formatValue(locale, fieldOptions) {
|
|
@@ -152,7 +153,8 @@ export function useDatePickerState(props: DatePickerOptions): DatePickerState {
|
|
|
152
153
|
granularity,
|
|
153
154
|
timeZone: defaultTimeZone,
|
|
154
155
|
hideTimeZone: props.hideTimeZone,
|
|
155
|
-
hourCycle: props.hourCycle
|
|
156
|
+
hourCycle: props.hourCycle,
|
|
157
|
+
showEra: value.calendar.identifier === 'gregory' && value.era === 'BC'
|
|
156
158
|
});
|
|
157
159
|
|
|
158
160
|
let formatter = new DateFormatter(locale, formatOptions);
|
|
@@ -10,14 +10,15 @@
|
|
|
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
|
-
import {
|
|
18
|
+
import {useOverlayTriggerState} from '@react-stately/overlays';
|
|
19
|
+
import {useState} from 'react';
|
|
19
20
|
|
|
20
|
-
export interface
|
|
21
|
+
export interface DateRangePickerStateOptions extends DateRangePickerProps<DateValue> {
|
|
21
22
|
/**
|
|
22
23
|
* Determines whether the date picker popover should close automatically when a date is selected.
|
|
23
24
|
* @default true
|
|
@@ -62,9 +63,7 @@ export interface DateRangePickerState {
|
|
|
62
63
|
/** The current validation state of the date picker, based on the `validationState`, `minValue`, and `maxValue` props. */
|
|
63
64
|
validationState: ValidationState,
|
|
64
65
|
/** Formats the selected range using the given options. */
|
|
65
|
-
formatValue(locale: string, fieldOptions: FieldOptions): string,
|
|
66
|
-
/** Replaces the start and/or end value of the selected range with the placeholder value if unentered. */
|
|
67
|
-
confirmPlaceholder(): void
|
|
66
|
+
formatValue(locale: string, fieldOptions: FieldOptions): {start: string, end: string}
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
/**
|
|
@@ -72,8 +71,8 @@ export interface DateRangePickerState {
|
|
|
72
71
|
* A date range picker combines two DateFields and a RangeCalendar popover to allow
|
|
73
72
|
* users to enter or select a date and time range.
|
|
74
73
|
*/
|
|
75
|
-
export function useDateRangePickerState(props:
|
|
76
|
-
let
|
|
74
|
+
export function useDateRangePickerState(props: DateRangePickerStateOptions): DateRangePickerState {
|
|
75
|
+
let overlayState = useOverlayTriggerState(props);
|
|
77
76
|
let [controlledValue, setControlledValue] = useControlledState<DateRange>(props.value, props.defaultValue || null, props.onChange);
|
|
78
77
|
let [placeholderValue, setPlaceholderValue] = useState(() => controlledValue || {start: null, end: null});
|
|
79
78
|
|
|
@@ -84,11 +83,8 @@ export function useDateRangePickerState(props: DateRangePickerOptions): DateRang
|
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
let value = controlledValue || placeholderValue;
|
|
87
|
-
let valueRef = useRef(value);
|
|
88
|
-
valueRef.current = value;
|
|
89
86
|
|
|
90
87
|
let setValue = (value: DateRange) => {
|
|
91
|
-
valueRef.current = value;
|
|
92
88
|
setPlaceholderValue(value);
|
|
93
89
|
if (value?.start && value.end) {
|
|
94
90
|
setControlledValue(value);
|
|
@@ -98,7 +94,7 @@ export function useDateRangePickerState(props: DateRangePickerOptions): DateRang
|
|
|
98
94
|
};
|
|
99
95
|
|
|
100
96
|
let v = (value?.start || value?.end || props.placeholderValue);
|
|
101
|
-
let [granularity
|
|
97
|
+
let [granularity] = useDefaultProps(v, props.granularity);
|
|
102
98
|
let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second' || granularity === 'millisecond';
|
|
103
99
|
let shouldCloseOnSelect = props.shouldCloseOnSelect ?? true;
|
|
104
100
|
|
|
@@ -138,7 +134,7 @@ export function useDateRangePickerState(props: DateRangePickerOptions): DateRang
|
|
|
138
134
|
}
|
|
139
135
|
|
|
140
136
|
if (shouldClose) {
|
|
141
|
-
setOpen(false);
|
|
137
|
+
overlayState.setOpen(false);
|
|
142
138
|
}
|
|
143
139
|
};
|
|
144
140
|
|
|
@@ -177,7 +173,7 @@ export function useDateRangePickerState(props: DateRangePickerOptions): DateRang
|
|
|
177
173
|
},
|
|
178
174
|
setDateRange,
|
|
179
175
|
setTimeRange,
|
|
180
|
-
isOpen,
|
|
176
|
+
isOpen: overlayState.isOpen,
|
|
181
177
|
setOpen(isOpen) {
|
|
182
178
|
// Commit the selected date range when the calendar is closed. Use a placeholder time if one wasn't set.
|
|
183
179
|
// If only the time range was set and not the date range, don't commit. The state will be preserved until
|
|
@@ -189,12 +185,12 @@ export function useDateRangePickerState(props: DateRangePickerOptions): DateRang
|
|
|
189
185
|
});
|
|
190
186
|
}
|
|
191
187
|
|
|
192
|
-
setOpen(isOpen);
|
|
188
|
+
overlayState.setOpen(isOpen);
|
|
193
189
|
},
|
|
194
190
|
validationState,
|
|
195
191
|
formatValue(locale, fieldOptions) {
|
|
196
192
|
if (!value || !value.start || !value.end) {
|
|
197
|
-
return
|
|
193
|
+
return null;
|
|
198
194
|
}
|
|
199
195
|
|
|
200
196
|
let startTimeZone = 'timeZone' in value.start ? value.start.timeZone : undefined;
|
|
@@ -206,17 +202,47 @@ export function useDateRangePickerState(props: DateRangePickerOptions): DateRang
|
|
|
206
202
|
granularity: startGranularity,
|
|
207
203
|
timeZone: startTimeZone,
|
|
208
204
|
hideTimeZone: props.hideTimeZone,
|
|
209
|
-
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')
|
|
210
208
|
});
|
|
211
209
|
|
|
210
|
+
let startDate = value.start.toDate(startTimeZone || 'UTC');
|
|
211
|
+
let endDate = value.end.toDate(endTimeZone || 'UTC');
|
|
212
|
+
|
|
212
213
|
let startFormatter = new DateFormatter(locale, startOptions);
|
|
213
214
|
let endFormatter: Intl.DateTimeFormat;
|
|
214
|
-
if (startTimeZone === endTimeZone && startGranularity === endGranularity) {
|
|
215
|
+
if (startTimeZone === endTimeZone && startGranularity === endGranularity && value.start.compare(value.end) !== 0) {
|
|
215
216
|
// Use formatRange, as it results in shorter output when some of the fields
|
|
216
217
|
// are shared between the start and end dates (e.g. the same month).
|
|
217
218
|
// Formatting will fail if the end date is before the start date. Fall back below when that happens.
|
|
218
219
|
try {
|
|
219
|
-
|
|
220
|
+
let parts = startFormatter.formatRangeToParts(startDate, endDate);
|
|
221
|
+
|
|
222
|
+
// Find the separator between the start and end date. This is determined
|
|
223
|
+
// by finding the last shared literal before the end range.
|
|
224
|
+
let separatorIndex = -1;
|
|
225
|
+
for (let i = 0; i < parts.length; i++) {
|
|
226
|
+
let part = parts[i];
|
|
227
|
+
if (part.source === 'shared' && part.type === 'literal') {
|
|
228
|
+
separatorIndex = i;
|
|
229
|
+
} else if (part.source === 'endRange') {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Now we can combine the parts into start and end strings.
|
|
235
|
+
let start = '';
|
|
236
|
+
let end = '';
|
|
237
|
+
for (let i = 0; i < parts.length; i++) {
|
|
238
|
+
if (i < separatorIndex) {
|
|
239
|
+
start += parts[i].value;
|
|
240
|
+
} else if (i > separatorIndex) {
|
|
241
|
+
end += parts[i].value;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {start, end};
|
|
220
246
|
} catch (e) {
|
|
221
247
|
// ignore
|
|
222
248
|
}
|
|
@@ -233,20 +259,10 @@ export function useDateRangePickerState(props: DateRangePickerOptions): DateRang
|
|
|
233
259
|
endFormatter = new DateFormatter(locale, endOptions);
|
|
234
260
|
}
|
|
235
261
|
|
|
236
|
-
return
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// a blur, which means the component won't have re-rendered yet.
|
|
241
|
-
let value = valueRef.current;
|
|
242
|
-
if (value && Boolean(value.start) !== Boolean(value.end)) {
|
|
243
|
-
let calendar = (value.start || value.end).calendar;
|
|
244
|
-
let placeholder = createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone);
|
|
245
|
-
setValue({
|
|
246
|
-
start: value.start || placeholder,
|
|
247
|
-
end: value.end || placeholder
|
|
248
|
-
});
|
|
249
|
-
}
|
|
262
|
+
return {
|
|
263
|
+
start: startFormatter.format(startDate),
|
|
264
|
+
end: endFormatter.format(endDate)
|
|
265
|
+
};
|
|
250
266
|
}
|
|
251
267
|
};
|
|
252
268
|
}
|
package/src/useTimeFieldState.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {getLocalTimeZone, GregorianCalendar, Time, toCalendarDateTime, today, to
|
|
|
16
16
|
import {useControlledState} from '@react-stately/utils';
|
|
17
17
|
import {useMemo} from 'react';
|
|
18
18
|
|
|
19
|
-
interface
|
|
19
|
+
export interface TimeFieldStateOptions extends TimePickerProps<TimeValue> {
|
|
20
20
|
/** The locale to display and edit the value according to. */
|
|
21
21
|
locale: string
|
|
22
22
|
}
|
|
@@ -26,7 +26,7 @@ interface TimeFieldProps extends TimePickerProps<TimeValue> {
|
|
|
26
26
|
* A time field allows users to enter and edit time values using a keyboard.
|
|
27
27
|
* Each part of a time value is displayed in an individually editable segment.
|
|
28
28
|
*/
|
|
29
|
-
export function useTimeFieldState(props:
|
|
29
|
+
export function useTimeFieldState(props: TimeFieldStateOptions): DateFieldState {
|
|
30
30
|
let {
|
|
31
31
|
placeholderValue = new Time(),
|
|
32
32
|
minValue,
|
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
|
|