@react-stately/datepicker 3.14.1 → 3.15.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.
@@ -42,6 +42,8 @@ export interface DateSegment {
42
42
  export interface DateFieldState extends FormValidationState {
43
43
  /** The current field value. */
44
44
  value: DateValue | null,
45
+ /** The default field value. */
46
+ defaultValue: DateValue | null,
45
47
  /** The current value, converted to a native JavaScript `Date` object. */
46
48
  dateValue: Date,
47
49
  /** The calendar system currently in use. */
@@ -118,9 +120,13 @@ const PAGE_STEP = {
118
120
  second: 15
119
121
  };
120
122
 
121
- // Node seems to convert everything to lowercase...
122
123
  const TYPE_MAPPING = {
123
- dayperiod: 'dayPeriod'
124
+ // Node seems to convert everything to lowercase...
125
+ dayperiod: 'dayPeriod',
126
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts#named_years
127
+ relatedYear: 'year',
128
+ yearName: 'literal', // not editable
129
+ unknown: 'literal'
124
130
  };
125
131
 
126
132
  export interface DateFieldStateOptions<T extends DateValue = DateValue> extends DatePickerProps<T> {
@@ -176,6 +182,7 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
176
182
  props.onChange
177
183
  );
178
184
 
185
+ let [initialValue] = useState(value);
179
186
  let calendarValue = useMemo(() => convertValue(value, calendar) ?? null, [value, calendar]);
180
187
 
181
188
  // We keep track of the placeholder date separately in state so that onChange is not called
@@ -207,7 +214,7 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
207
214
  let allSegments: Partial<typeof EDITABLE_SEGMENTS> = useMemo(() =>
208
215
  dateFormatter.formatToParts(new Date())
209
216
  .filter(seg => EDITABLE_SEGMENTS[seg.type])
210
- .reduce((p, seg) => (p[seg.type] = true, p), {})
217
+ .reduce((p, seg) => (p[TYPE_MAPPING[seg.type] || seg.type] = true, p), {})
211
218
  , [dateFormatter]);
212
219
 
213
220
  let [validSegments, setValidSegments] = useState<Partial<typeof EDITABLE_SEGMENTS>>(
@@ -256,7 +263,18 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
256
263
  setDate(null);
257
264
  setPlaceholderDate(createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone));
258
265
  setValidSegments({});
259
- } else if (validKeys.length >= allKeys.length || (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod && clearedSegment.current !== 'dayPeriod')) {
266
+ } else if (
267
+ (validKeys.length === 0 && clearedSegment.current == null) ||
268
+ validKeys.length >= allKeys.length ||
269
+ (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod && clearedSegment.current !== 'dayPeriod')
270
+ ) {
271
+ // If the field was empty (no valid segments) or all segments are completed, commit the new value.
272
+ // When committing from an empty state, mark every segment as valid so value is committed.
273
+ if (validKeys.length === 0) {
274
+ validSegments = {...allSegments};
275
+ setValidSegments(validSegments);
276
+ }
277
+
260
278
  // The display calendar should not have any effect on the emitted value.
261
279
  // Emit dates in the same calendar as the original value, if any, otherwise gregorian.
262
280
  newValue = toCalendar(newValue, v?.calendar || new GregorianCalendar());
@@ -268,8 +286,8 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
268
286
  };
269
287
 
270
288
  let dateValue = useMemo(() => displayValue.toDate(timeZone), [displayValue, timeZone]);
271
- let segments = useMemo(() =>
272
- processSegments(dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity),
289
+ let segments = useMemo(() =>
290
+ processSegments(dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity),
273
291
  [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale, granularity]);
274
292
 
275
293
  // When the era field appears, mark it valid if the year field is already valid.
@@ -323,6 +341,7 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
323
341
  return {
324
342
  ...validation,
325
343
  value: calendarValue,
344
+ defaultValue: props.defaultValue ?? initialValue,
326
345
  dateValue,
327
346
  calendar,
328
347
  setValue,
@@ -413,18 +432,19 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
413
432
  let segments = dateFormatter.formatToParts(dateValue);
414
433
  let processedSegments: DateSegment[] = [];
415
434
  for (let segment of segments) {
416
- let isEditable = EDITABLE_SEGMENTS[segment.type];
417
- if (segment.type === 'era' && calendar.getEras().length === 1) {
435
+ let type = TYPE_MAPPING[segment.type] || segment.type;
436
+ let isEditable = EDITABLE_SEGMENTS[type];
437
+ if (type === 'era' && calendar.getEras().length === 1) {
418
438
  isEditable = false;
419
439
  }
420
440
 
421
- let isPlaceholder = EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type];
422
- let placeholder = EDITABLE_SEGMENTS[segment.type] ? getPlaceholder(segment.type, segment.value, locale) : null;
441
+ let isPlaceholder = EDITABLE_SEGMENTS[type] && !validSegments[type];
442
+ let placeholder = EDITABLE_SEGMENTS[type] ? getPlaceholder(type, segment.value, locale) : null;
423
443
 
424
444
  let dateSegment = {
425
- type: TYPE_MAPPING[segment.type] || segment.type,
445
+ type,
426
446
  text: isPlaceholder ? placeholder : segment.value,
427
- ...getSegmentLimits(displayValue, segment.type, resolvedOptions),
447
+ ...getSegmentLimits(displayValue, type, resolvedOptions),
428
448
  isPlaceholder,
429
449
  placeholder,
430
450
  isEditable
@@ -432,9 +452,9 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
432
452
 
433
453
  // There is an issue in RTL languages where time fields render (minute:hour) instead of (hour:minute).
434
454
  // To force an LTR direction on the time field since, we wrap the time segments in LRI (left-to-right) isolate unicode. See https://www.w3.org/International/questions/qa-bidi-unicode-controls.
435
- // These unicode characters will be added to the array of processed segments as literals and will mark the start and end of the embedded direction change.
436
- if (segment.type === 'hour') {
437
- // This marks the start of the embedded direction change.
455
+ // These unicode characters will be added to the array of processed segments as literals and will mark the start and end of the embedded direction change.
456
+ if (type === 'hour') {
457
+ // This marks the start of the embedded direction change.
438
458
  processedSegments.push({
439
459
  type: 'literal',
440
460
  text: '\u2066',
@@ -445,7 +465,7 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
445
465
  });
446
466
  processedSegments.push(dateSegment);
447
467
  // This marks the end of the embedded direction change in the case that the granularity it set to "hour".
448
- if (segment.type === granularity) {
468
+ if (type === granularity) {
449
469
  processedSegments.push({
450
470
  type: 'literal',
451
471
  text: '\u2069',
@@ -455,7 +475,7 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
455
475
  isEditable: false
456
476
  });
457
477
  }
458
- } else if (timeValue.includes(segment.type) && segment.type === granularity) {
478
+ } else if (timeValue.includes(type) && type === granularity) {
459
479
  processedSegments.push(dateSegment);
460
480
  // This marks the end of the embedded direction change.
461
481
  processedSegments.push({
@@ -467,7 +487,7 @@ function processSegments(dateValue, validSegments, dateFormatter, resolvedOption
467
487
  isEditable: false
468
488
  });
469
489
  } else {
470
- // We only want to "wrap" the unicode around segments that are hour, minute, or second. If they aren't, just process as normal.
490
+ // We only want to "wrap" the unicode around segments that are hour, minute, or second. If they aren't, just process as normal.
471
491
  processedSegments.push(dateSegment);
472
492
  }
473
493
  }
@@ -30,6 +30,8 @@ export interface DatePickerStateOptions<T extends DateValue> extends DatePickerP
30
30
  export interface DatePickerState extends OverlayTriggerState, FormValidationState {
31
31
  /** The currently selected date. */
32
32
  value: DateValue | null,
33
+ /** The default date. */
34
+ defaultValue: DateValue | null,
33
35
  /** Sets the selected date. */
34
36
  setValue(value: DateValue | null): void,
35
37
  /**
@@ -74,6 +76,7 @@ export interface DatePickerState extends OverlayTriggerState, FormValidationStat
74
76
  export function useDatePickerState<T extends DateValue = DateValue>(props: DatePickerStateOptions<T>): DatePickerState {
75
77
  let overlayState = useOverlayTriggerState(props);
76
78
  let [value, setValue] = useControlledState<DateValue | null, MappedDateValue<T> | null>(props.value, props.defaultValue || null, props.onChange);
79
+ let [initialValue] = useState(value);
77
80
 
78
81
  let v = (value || props.placeholderValue || null);
79
82
  let [granularity, defaultTimeZone] = useDefaultProps(v, props.granularity);
@@ -161,6 +164,7 @@ export function useDatePickerState<T extends DateValue = DateValue>(props: DateP
161
164
  return {
162
165
  ...validation,
163
166
  value,
167
+ defaultValue: props.defaultValue ?? initialValue,
164
168
  setValue,
165
169
  dateValue: selectedDate,
166
170
  timeValue: selectedTime,
@@ -32,6 +32,8 @@ type TimeRange = RangeValue<TimeValue>;
32
32
  export interface DateRangePickerState extends OverlayTriggerState, FormValidationState {
33
33
  /** The currently selected date range. */
34
34
  value: RangeValue<DateValue | null>,
35
+ /** The default selected date range. */
36
+ defaultValue: DateRange | null,
35
37
  /** Sets the selected date range. */
36
38
  setValue(value: DateRange | null): void,
37
39
  /**
@@ -83,6 +85,7 @@ export interface DateRangePickerState extends OverlayTriggerState, FormValidatio
83
85
  export function useDateRangePickerState<T extends DateValue = DateValue>(props: DateRangePickerStateOptions<T>): DateRangePickerState {
84
86
  let overlayState = useOverlayTriggerState(props);
85
87
  let [controlledValue, setControlledValue] = useControlledState<DateRange | null, RangeValue<MappedDateValue<T>> | null>(props.value, props.defaultValue || null, props.onChange);
88
+ let [initialValue] = useState(controlledValue);
86
89
  let [placeholderValue, setPlaceholderValue] = useState<RangeValue<DateValue | null>>(() => controlledValue || {start: null, end: null});
87
90
 
88
91
  // Reset the placeholder if the value prop is set to null.
@@ -93,8 +96,9 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
93
96
 
94
97
  let value = controlledValue || placeholderValue;
95
98
 
96
- let setValue = (value: RangeValue<DateValue | null> | null) => {
97
- setPlaceholderValue(value || {start: null, end: null});
99
+ let setValue = (newValue: RangeValue<DateValue | null> | null) => {
100
+ value = newValue || {start: null, end: null};
101
+ setPlaceholderValue(value);
98
102
  if (isCompleteRange(value)) {
99
103
  setControlledValue(value);
100
104
  } else {
@@ -131,7 +135,8 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
131
135
  let setDateRange = (range: RangeValue<DateValue | null>) => {
132
136
  let shouldClose = typeof shouldCloseOnSelect === 'function' ? shouldCloseOnSelect() : shouldCloseOnSelect;
133
137
  if (hasTime) {
134
- if (isCompleteRange(range) && timeRange?.start && timeRange?.end) {
138
+ // Set a placeholder time if the popover is closing so we don't leave the field in an incomplete state.
139
+ if (isCompleteRange(range) && (shouldClose || (timeRange?.start && timeRange?.end))) {
135
140
  commitValue(range, {
136
141
  start: timeRange?.start || getPlaceholderTime(props.placeholderValue),
137
142
  end: timeRange?.end || getPlaceholderTime(props.placeholderValue)
@@ -191,6 +196,7 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
191
196
  return {
192
197
  ...validation,
193
198
  value,
199
+ defaultValue: props.defaultValue ?? initialValue,
194
200
  setValue,
195
201
  dateRange,
196
202
  timeRange,
@@ -36,19 +36,20 @@ export function useTimeFieldState<T extends TimeValue = TimeValue>(props: TimeFi
36
36
  placeholderValue = new Time(),
37
37
  minValue,
38
38
  maxValue,
39
+ defaultValue,
39
40
  granularity,
40
41
  validate
41
42
  } = props;
42
43
 
43
44
  let [value, setValue] = useControlledState<TimeValue | null, MappedTimeValue<T> | null>(
44
45
  props.value,
45
- props.defaultValue ?? null,
46
+ defaultValue ?? null,
46
47
  props.onChange
47
48
  );
48
49
 
49
50
  let v = value || placeholderValue;
50
51
  let day = v && 'day' in v ? v : undefined;
51
- let defaultValueTimeZone = props.defaultValue && 'timeZone' in props.defaultValue ? props.defaultValue.timeZone : undefined;
52
+ let defaultValueTimeZone = defaultValue && 'timeZone' in defaultValue ? defaultValue.timeZone : undefined;
52
53
  let placeholderDate = useMemo(() => {
53
54
  let valueTimeZone = v && 'timeZone' in v ? v.timeZone : undefined;
54
55
 
@@ -59,6 +60,7 @@ export function useTimeFieldState<T extends TimeValue = TimeValue>(props: TimeFi
59
60
 
60
61
  let timeValue = useMemo(() => value && 'day' in value ? toTime(value) : value as Time, [value]);
61
62
  let dateTime = useMemo(() => value == null ? null : convertValue(value), [value]);
63
+ let defaultDateTime = useMemo(() => defaultValue == null ? null : convertValue(defaultValue), [defaultValue]);
62
64
  let onChange = newValue => {
63
65
  setValue(day || defaultValueTimeZone ? newValue : newValue && toTime(newValue));
64
66
  };
@@ -66,7 +68,7 @@ export function useTimeFieldState<T extends TimeValue = TimeValue>(props: TimeFi
66
68
  let state = useDateFieldState({
67
69
  ...props,
68
70
  value: dateTime,
69
- defaultValue: undefined,
71
+ defaultValue: defaultDateTime,
70
72
  minValue: minDate,
71
73
  maxValue: maxDate,
72
74
  onChange,