@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.
@@ -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
- * Replaces the value of the date field with the placeholder value.
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
- }), [props.maxGranularity, granularity, props.hourCycle, defaultTimeZone, hideTimeZone]);
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: EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type],
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
- 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
+ }
260
279
 
261
280
  let markValid = (part: Intl.DateTimeFormatPartTypes) => {
262
281
  validSegments[part] = true;
263
- if (part === 'year' && hasEra) {
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
- markValid(type);
271
- 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
+ }
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(part) {
331
+ confirmPlaceholder() {
306
332
  if (props.isDisabled || props.isReadOnly) {
307
333
  return;
308
334
  }
309
335
 
310
- if (!part) {
311
- // Confirm the rest of the placeholder if any of the segments are valid.
312
- let numValid = Object.keys(validSegments).length;
313
- if (numValid > 0 && numValid < Object.keys(allSegments).length) {
314
- validSegments = {...allSegments};
315
- setValidSegments(validSegments);
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 {useRef, useState} from 'react';
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, defaultTimeZone] = useDefaultProps(v, props.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