@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.
@@ -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
+ }
@@ -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
- * Replaces the value of the date field with the placeholder value.
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 DatePickerFieldProps extends DatePickerProps<DateValue> {
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: DatePickerFieldProps): DateFieldState {
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
- }), [props.maxGranularity, granularity, props.hourCycle, defaultTimeZone, hideTimeZone]);
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: EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type],
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
- let hasEra = useMemo(() => segments.some(s => s.type === 'era'), [segments]);
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' && hasEra) {
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
- markValid(type);
269
- setValue(addSegment(displayValue, type, amount, resolvedOptions));
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(part) {
331
+ confirmPlaceholder() {
303
332
  if (props.isDisabled || props.isReadOnly) {
304
333
  return;
305
334
  }
306
335
 
307
- if (!part) {
308
- // Confirm the rest of the placeholder if any of the segments are valid.
309
- let numValid = Object.keys(validSegments).length;
310
- if (numValid > 0 && numValid < Object.keys(allSegments).length) {
311
- validSegments = {...allSegments};
312
- setValidSegments(validSegments);
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 DatePickerOptions extends DatePickerProps<DateValue> {
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: DatePickerOptions): DatePickerState {
67
- let [isOpen, setOpen] = useState(false);
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 {useRef, useState} from 'react';
18
+ import {useOverlayTriggerState} from '@react-stately/overlays';
19
+ import {useState} from 'react';
19
20
 
20
- export interface DateRangePickerOptions extends DateRangePickerProps<DateValue> {
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: DateRangePickerOptions): DateRangePickerState {
76
- let [isOpen, setOpen] = useState(false);
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, defaultTimeZone] = useDefaultProps(v, props.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
- return startFormatter.formatRange(value.start.toDate(startTimeZone), value.end.toDate(endTimeZone));
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 `${startFormatter.format(value.start.toDate(startTimeZone))} – ${endFormatter.format(value.end.toDate(endTimeZone))}`;
237
- },
238
- confirmPlaceholder() {
239
- // Need to use ref value here because the value can be set in the same tick as
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
  }
@@ -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 TimeFieldProps extends TimePickerProps<TimeValue> {
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: TimeFieldProps): DateFieldState {
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