@react-stately/datepicker 3.0.0-rc.1 → 3.0.2

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. */
@@ -189,7 +192,7 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
189
192
 
190
193
  // Determine how many editable segments there are for validation purposes.
191
194
  // The result is cached for performance.
192
- let allSegments = useMemo(() =>
195
+ let allSegments: Partial<typeof EDITABLE_SEGMENTS> = useMemo(() =>
193
196
  dateFormatter.formatToParts(new Date())
194
197
  .filter(seg => EDITABLE_SEGMENTS[seg.type])
195
198
  .reduce((p, seg) => (p[seg.type] = true, p), {})
@@ -251,29 +254,46 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
251
254
  isEditable = false;
252
255
  }
253
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;
254
259
  return {
255
260
  type: TYPE_MAPPING[segment.type] || segment.type,
256
- text: segment.value,
261
+ text: isPlaceholder ? placeholder : segment.value,
257
262
  ...getSegmentLimits(displayValue, segment.type, resolvedOptions),
258
- isPlaceholder: EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type],
263
+ isPlaceholder,
264
+ placeholder,
259
265
  isEditable
260
266
  } as DateSegment;
261
267
  })
262
- , [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar]);
268
+ , [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale]);
263
269
 
264
- 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
+ }
265
279
 
266
280
  let markValid = (part: Intl.DateTimeFormatPartTypes) => {
267
281
  validSegments[part] = true;
268
- if (part === 'year' && hasEra) {
282
+ if (part === 'year' && allSegments.era) {
269
283
  validSegments.era = true;
270
284
  }
271
285
  setValidSegments({...validSegments});
272
286
  };
273
287
 
274
288
  let adjustSegment = (type: Intl.DateTimeFormatPartTypes, amount: number) => {
275
- markValid(type);
276
- 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
+ }
277
297
  };
278
298
 
279
299
  let validationState: ValidationState = props.validationState ||
@@ -282,6 +302,7 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
282
302
  return {
283
303
  value: calendarValue,
284
304
  dateValue,
305
+ calendar,
285
306
  setValue,
286
307
  segments,
287
308
  dateFormatter,
@@ -307,21 +328,17 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
307
328
  markValid(part);
308
329
  setValue(setSegment(displayValue, part, v, resolvedOptions));
309
330
  },
310
- confirmPlaceholder(part) {
331
+ confirmPlaceholder() {
311
332
  if (props.isDisabled || props.isReadOnly) {
312
333
  return;
313
334
  }
314
335
 
315
- if (!part) {
316
- // Confirm the rest of the placeholder if any of the segments are valid.
317
- let numValid = Object.keys(validSegments).length;
318
- if (numValid > 0 && numValid < Object.keys(allSegments).length) {
319
- validSegments = {...allSegments};
320
- setValidSegments(validSegments);
321
- setValue(displayValue.copy());
322
- }
323
- } else if (!validSegments[part]) {
324
- 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);
325
342
  setValue(displayValue.copy());
326
343
  }
327
344
  },
@@ -463,6 +480,7 @@ function setSegment(value: DateValue, part: string, segmentValue: number, option
463
480
  case 'day':
464
481
  case 'month':
465
482
  case 'year':
483
+ case 'era':
466
484
  return value.set({[part]: segmentValue});
467
485
  }
468
486
 
@@ -71,7 +71,7 @@ export function useDatePickerState(props: DatePickerStateOptions): DatePickerSta
71
71
  let v = (value || props.placeholderValue);
72
72
  let [granularity, defaultTimeZone] = useDefaultProps(v, props.granularity);
73
73
  let dateValue = value != null ? value.toDate(defaultTimeZone ?? 'UTC') : null;
74
- let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second' || granularity === 'millisecond';
74
+ let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second';
75
75
  let shouldCloseOnSelect = props.shouldCloseOnSelect ?? true;
76
76
 
77
77
  let [selectedDate, setSelectedDate] = useState<DateValue>(null);
@@ -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,8 +94,8 @@ 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);
103
- let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second' || granularity === 'millisecond';
97
+ let [granularity] = useDefaultProps(v, props.granularity);
98
+ let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second';
104
99
  let shouldCloseOnSelect = props.shouldCloseOnSelect ?? true;
105
100
 
106
101
  let [dateRange, setSelectedDateRange] = useState<DateRange>(null);
@@ -268,19 +263,6 @@ export function useDateRangePickerState(props: DateRangePickerStateOptions): Dat
268
263
  start: startFormatter.format(startDate),
269
264
  end: endFormatter.format(endDate)
270
265
  };
271
- },
272
- confirmPlaceholder() {
273
- // Need to use ref value here because the value can be set in the same tick as
274
- // a blur, which means the component won't have re-rendered yet.
275
- let value = valueRef.current;
276
- if (value && Boolean(value.start) !== Boolean(value.end)) {
277
- let calendar = (value.start || value.end).calendar;
278
- let placeholder = createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone);
279
- setValue({
280
- start: value.start || placeholder,
281
- end: value.end || placeholder
282
- });
283
- }
284
266
  }
285
267
  };
286
268
  }