@react-stately/datepicker 3.10.3 → 3.11.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.
@@ -12,7 +12,7 @@
12
12
 
13
13
 
14
14
  import {DateFormatter, toCalendarDate, toCalendarDateTime} from '@internationalized/date';
15
- import {DateRange, DateRangePickerProps, DateValue, Granularity, TimeValue} from '@react-types/datepicker';
15
+ import {DateRange, DateRangePickerProps, DateValue, Granularity, MappedDateValue, TimeValue} from '@react-types/datepicker';
16
16
  import {FieldOptions, FormatterOptions, getFormatOptions, getPlaceholderTime, getRangeValidationResult, useDefaultProps} from './utils';
17
17
  import {FormValidationState, useFormValidationState} from '@react-stately/form';
18
18
  import {OverlayTriggerState, useOverlayTriggerState} from '@react-stately/overlays';
@@ -31,29 +31,29 @@ export interface DateRangePickerStateOptions<T extends DateValue = DateValue> ex
31
31
  type TimeRange = RangeValue<TimeValue>;
32
32
  export interface DateRangePickerState extends OverlayTriggerState, FormValidationState {
33
33
  /** The currently selected date range. */
34
- value: DateRange | null,
34
+ value: RangeValue<DateValue | null>,
35
35
  /** Sets the selected date range. */
36
36
  setValue(value: DateRange | null): void,
37
37
  /**
38
38
  * The date portion of the selected range. This may be set prior to `value` if the user has
39
39
  * selected a date range but has not yet selected a time range.
40
40
  */
41
- dateRange: DateRange | null,
41
+ dateRange: RangeValue<DateValue | null> | null,
42
42
  /** Sets the date portion of the selected range. */
43
43
  setDateRange(value: DateRange): void,
44
44
  /**
45
45
  * The time portion of the selected range. This may be set prior to `value` if the user has
46
46
  * selected a time range but has not yet selected a date range.
47
47
  */
48
- timeRange: TimeRange | null,
48
+ timeRange: RangeValue<TimeValue | null> | null,
49
49
  /** Sets the time portion of the selected range. */
50
50
  setTimeRange(value: TimeRange): void,
51
51
  /** Sets the date portion of either the start or end of the selected range. */
52
- setDate(part: 'start' | 'end', value: DateValue): void,
52
+ setDate(part: 'start' | 'end', value: DateValue | null): void,
53
53
  /** Sets the time portion of either the start or end of the selected range. */
54
- setTime(part: 'start' | 'end', value: TimeValue): void,
54
+ setTime(part: 'start' | 'end', value: TimeValue | null): void,
55
55
  /** Sets the date and time of either the start or end of the selected range. */
56
- setDateTime(part: 'start' | 'end', value: DateValue): void,
56
+ setDateTime(part: 'start' | 'end', value: DateValue | null): void,
57
57
  /** The granularity for the field, based on the `granularity` prop and current value. */
58
58
  granularity: Granularity,
59
59
  /** Whether the date range picker supports selecting times, according to the `granularity` prop and current value. */
@@ -66,11 +66,11 @@ export interface DateRangePickerState extends OverlayTriggerState, FormValidatio
66
66
  * The current validation state of the date range picker, based on the `validationState`, `minValue`, and `maxValue` props.
67
67
  * @deprecated Use `isInvalid` instead.
68
68
  */
69
- validationState: ValidationState,
69
+ validationState: ValidationState | null,
70
70
  /** Whether the date range picker is invalid, based on the `isInvalid`, `minValue`, and `maxValue` props. */
71
71
  isInvalid: boolean,
72
72
  /** Formats the selected range using the given options. */
73
- formatValue(locale: string, fieldOptions: FieldOptions): {start: string, end: string},
73
+ formatValue(locale: string, fieldOptions: FieldOptions): {start: string, end: string} | null,
74
74
  /** Gets a formatter based on state's props. */
75
75
  getDateFormatter(locale: string, formatOptions: FormatterOptions): DateFormatter
76
76
  }
@@ -82,8 +82,8 @@ export interface DateRangePickerState extends OverlayTriggerState, FormValidatio
82
82
  */
83
83
  export function useDateRangePickerState<T extends DateValue = DateValue>(props: DateRangePickerStateOptions<T>): DateRangePickerState {
84
84
  let overlayState = useOverlayTriggerState(props);
85
- let [controlledValue, setControlledValue] = useControlledState<DateRange>(props.value, props.defaultValue || null, props.onChange);
86
- let [placeholderValue, setPlaceholderValue] = useState(() => controlledValue || {start: null, end: null});
85
+ let [controlledValue, setControlledValue] = useControlledState<DateRange | null, RangeValue<MappedDateValue<T>> | null>(props.value, props.defaultValue || null, props.onChange);
86
+ let [placeholderValue, setPlaceholderValue] = useState<RangeValue<DateValue | null>>(() => controlledValue || {start: null, end: null});
87
87
 
88
88
  // Reset the placeholder if the value prop is set to null.
89
89
  if (controlledValue == null && placeholderValue.start && placeholderValue.end) {
@@ -93,24 +93,24 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
93
93
 
94
94
  let value = controlledValue || placeholderValue;
95
95
 
96
- let setValue = (value: DateRange) => {
96
+ let setValue = (value: RangeValue<DateValue | null> | null) => {
97
97
  setPlaceholderValue(value || {start: null, end: null});
98
- if (value?.start && value.end) {
98
+ if (isCompleteRange(value)) {
99
99
  setControlledValue(value);
100
100
  } else {
101
101
  setControlledValue(null);
102
102
  }
103
103
  };
104
104
 
105
- let v = (value?.start || value?.end || props.placeholderValue);
105
+ let v = (value?.start || value?.end || props.placeholderValue || null);
106
106
  let [granularity, defaultTimeZone] = useDefaultProps(v, props.granularity);
107
107
  let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second';
108
108
  let shouldCloseOnSelect = props.shouldCloseOnSelect ?? true;
109
109
 
110
- let [dateRange, setSelectedDateRange] = useState<DateRange>(null);
111
- let [timeRange, setSelectedTimeRange] = useState<TimeRange>(null);
110
+ let [dateRange, setSelectedDateRange] = useState<RangeValue<DateValue | null> | null>(null);
111
+ let [timeRange, setSelectedTimeRange] = useState<RangeValue<TimeValue | null> | null>(null);
112
112
 
113
- if (value && value.start && value.end) {
113
+ if (value && isCompleteRange(value)) {
114
114
  dateRange = value;
115
115
  if ('hour' in value.start) {
116
116
  timeRange = value as TimeRange;
@@ -128,10 +128,10 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
128
128
  };
129
129
 
130
130
  // Intercept setValue to make sure the Time section is not changed by date selection in Calendar
131
- let setDateRange = (range: DateRange) => {
131
+ let setDateRange = (range: RangeValue<DateValue | null>) => {
132
132
  let shouldClose = typeof shouldCloseOnSelect === 'function' ? shouldCloseOnSelect() : shouldCloseOnSelect;
133
133
  if (hasTime) {
134
- if (shouldClose || (range.start && range.end && timeRange?.start && timeRange?.end)) {
134
+ if (isCompleteRange(range) && timeRange?.start && timeRange?.end) {
135
135
  commitValue(range, {
136
136
  start: timeRange?.start || getPlaceholderTime(props.placeholderValue),
137
137
  end: timeRange?.end || getPlaceholderTime(props.placeholderValue)
@@ -139,7 +139,7 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
139
139
  } else {
140
140
  setSelectedDateRange(range);
141
141
  }
142
- } else if (range.start && range.end) {
142
+ } else if (isCompleteRange(range)) {
143
143
  setValue(range);
144
144
  validation.commitValidation();
145
145
  } else {
@@ -151,8 +151,8 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
151
151
  }
152
152
  };
153
153
 
154
- let setTimeRange = (range: TimeRange) => {
155
- if (dateRange?.start && dateRange?.end && range.start && range.end) {
154
+ let setTimeRange = (range: RangeValue<TimeValue | null>) => {
155
+ if (isCompleteRange(dateRange) && isCompleteRange(range)) {
156
156
  commitValue(dateRange, range);
157
157
  } else {
158
158
  setSelectedTimeRange(range);
@@ -180,13 +180,13 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
180
180
 
181
181
  let validation = useFormValidationState({
182
182
  ...props,
183
- value: controlledValue,
184
- name: useMemo(() => [props.startName, props.endName], [props.startName, props.endName]),
183
+ value: controlledValue as RangeValue<MappedDateValue<T>> | null,
184
+ name: useMemo(() => [props.startName, props.endName].filter(n => n != null), [props.startName, props.endName]),
185
185
  builtinValidation
186
186
  });
187
187
 
188
188
  let isValueInvalid = validation.displayValidation.isInvalid;
189
- let validationState: ValidationState = props.validationState || (isValueInvalid ? 'invalid' : null);
189
+ let validationState: ValidationState | null = props.validationState || (isValueInvalid ? 'invalid' : null);
190
190
 
191
191
  return {
192
192
  ...validation,
@@ -197,13 +197,25 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
197
197
  granularity,
198
198
  hasTime,
199
199
  setDate(part, date) {
200
- setDateRange({...dateRange, [part]: date});
200
+ if (part === 'start') {
201
+ setDateRange({start: date, end: dateRange?.end ?? null});
202
+ } else {
203
+ setDateRange({start: dateRange?.start ?? null, end: date});
204
+ }
201
205
  },
202
206
  setTime(part, time) {
203
- setTimeRange({...timeRange, [part]: time});
207
+ if (part === 'start') {
208
+ setTimeRange({start: time, end: timeRange?.end ?? null});
209
+ } else {
210
+ setTimeRange({start: timeRange?.start ?? null, end: time});
211
+ }
204
212
  },
205
213
  setDateTime(part, dateTime) {
206
- setValue({...value, [part]: dateTime});
214
+ if (part === 'start') {
215
+ setValue({start: dateTime, end: value?.end ?? null});
216
+ } else {
217
+ setValue({start: value?.start ?? null, end: dateTime});
218
+ }
207
219
  },
208
220
  setDateRange,
209
221
  setTimeRange,
@@ -212,7 +224,7 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
212
224
  // Commit the selected date range when the calendar is closed. Use a placeholder time if one wasn't set.
213
225
  // If only the time range was set and not the date range, don't commit. The state will be preserved until
214
226
  // the user opens the popover again.
215
- if (!isOpen && !(value?.start && value?.end) && dateRange?.start && dateRange?.end && hasTime) {
227
+ if (!isOpen && !(value?.start && value?.end) && isCompleteRange(dateRange) && hasTime) {
216
228
  commitValue(dateRange, {
217
229
  start: timeRange?.start || getPlaceholderTime(props.placeholderValue),
218
230
  end: timeRange?.end || getPlaceholderTime(props.placeholderValue)
@@ -278,7 +290,7 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
278
290
  }
279
291
 
280
292
  return {start, end};
281
- } catch (e) {
293
+ } catch {
282
294
  // ignore
283
295
  }
284
296
 
@@ -306,3 +318,7 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
306
318
  }
307
319
  };
308
320
  }
321
+
322
+ function isCompleteRange<T>(value: RangeValue<T | null> | null): value is RangeValue<T> {
323
+ return value?.start != null && value.end != null;
324
+ }
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import {DateFieldState, useDateFieldState} from '.';
14
- import {DateValue, TimePickerProps, TimeValue} from '@react-types/datepicker';
14
+ import {DateValue, MappedTimeValue, TimePickerProps, TimeValue} from '@react-types/datepicker';
15
15
  import {getLocalTimeZone, GregorianCalendar, Time, toCalendarDateTime, today, toTime, toZoned} from '@internationalized/date';
16
16
  import {useCallback, useMemo} from 'react';
17
17
  import {useControlledState} from '@react-stately/utils';
@@ -40,9 +40,9 @@ export function useTimeFieldState<T extends TimeValue = TimeValue>(props: TimeFi
40
40
  validate
41
41
  } = props;
42
42
 
43
- let [value, setValue] = useControlledState<TimeValue>(
43
+ let [value, setValue] = useControlledState<TimeValue | null, MappedTimeValue<T> | null>(
44
44
  props.value,
45
- props.defaultValue,
45
+ props.defaultValue ?? null,
46
46
  props.onChange
47
47
  );
48
48
 
@@ -52,7 +52,7 @@ export function useTimeFieldState<T extends TimeValue = TimeValue>(props: TimeFi
52
52
  let placeholderDate = useMemo(() => {
53
53
  let valueTimeZone = v && 'timeZone' in v ? v.timeZone : undefined;
54
54
 
55
- return (valueTimeZone || defaultValueTimeZone) && placeholderValue ? toZoned(convertValue(placeholderValue), valueTimeZone || defaultValueTimeZone) : convertValue(placeholderValue);
55
+ return (valueTimeZone || defaultValueTimeZone) && placeholderValue ? toZoned(convertValue(placeholderValue)!, valueTimeZone || defaultValueTimeZone!) : convertValue(placeholderValue);
56
56
  }, [placeholderValue, v, defaultValueTimeZone]);
57
57
  let minDate = useMemo(() => convertValue(minValue, day), [minValue, day]);
58
58
  let maxDate = useMemo(() => convertValue(maxValue, day), [maxValue, day]);
@@ -72,7 +72,7 @@ export function useTimeFieldState<T extends TimeValue = TimeValue>(props: TimeFi
72
72
  onChange,
73
73
  granularity: granularity || 'minute',
74
74
  maxGranularity: 'hour',
75
- placeholderValue: placeholderDate,
75
+ placeholderValue: placeholderDate ?? undefined,
76
76
  // Calendar should not matter for time fields.
77
77
  createCalendar: () => new GregorianCalendar(),
78
78
  validate: useCallback(() => validate?.(value as any), [validate, value])
@@ -84,7 +84,7 @@ export function useTimeFieldState<T extends TimeValue = TimeValue>(props: TimeFi
84
84
  };
85
85
  }
86
86
 
87
- function convertValue(value: TimeValue, date: DateValue = today(getLocalTimeZone())) {
87
+ function convertValue(value: TimeValue | null | undefined, date: DateValue = today(getLocalTimeZone())) {
88
88
  if (!value) {
89
89
  return null;
90
90
  }
package/src/utils.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {Calendar, DateFormatter, now, Time, toCalendar, toCalendarDate, toCalendarDateTime} from '@internationalized/date';
13
+ import {Calendar, DateFormatter, getLocalTimeZone, now, Time, toCalendar, toCalendarDate, toCalendarDateTime} from '@internationalized/date';
14
14
  import {DatePickerProps, DateValue, Granularity, TimeValue} from '@react-types/datepicker';
15
15
  // @ts-ignore
16
16
  import i18nMessages from '../intl/*.json';
@@ -29,17 +29,17 @@ function getLocale() {
29
29
  }
30
30
 
31
31
  export function getValidationResult(
32
- value: DateValue,
33
- minValue: DateValue,
34
- maxValue: DateValue,
35
- isDateUnavailable: (v: DateValue) => boolean,
32
+ value: DateValue | null,
33
+ minValue: DateValue | null | undefined,
34
+ maxValue: DateValue | null | undefined,
35
+ isDateUnavailable: ((v: DateValue) => boolean) | undefined,
36
36
  options: FormatterOptions
37
37
  ): ValidationResult {
38
38
  let rangeOverflow = value != null && maxValue != null && value.compare(maxValue) > 0;
39
39
  let rangeUnderflow = value != null && minValue != null && value.compare(minValue) < 0;
40
40
  let isUnavailable = (value != null && isDateUnavailable?.(value)) || false;
41
41
  let isInvalid = rangeOverflow || rangeUnderflow || isUnavailable;
42
- let errors = [];
42
+ let errors: string[] = [];
43
43
 
44
44
  if (isInvalid) {
45
45
  let locale = getLocale();
@@ -48,11 +48,11 @@ export function getValidationResult(
48
48
  let dateFormatter = new DateFormatter(locale, getFormatOptions({}, options));
49
49
  let timeZone = dateFormatter.resolvedOptions().timeZone;
50
50
 
51
- if (rangeUnderflow) {
51
+ if (rangeUnderflow && minValue != null) {
52
52
  errors.push(formatter.format('rangeUnderflow', {minValue: dateFormatter.format(minValue.toDate(timeZone))}));
53
53
  }
54
54
 
55
- if (rangeOverflow) {
55
+ if (rangeOverflow && maxValue != null) {
56
56
  errors.push(formatter.format('rangeOverflow', {maxValue: dateFormatter.format(maxValue.toDate(timeZone))}));
57
57
  }
58
58
 
@@ -81,14 +81,14 @@ export function getValidationResult(
81
81
  }
82
82
 
83
83
  export function getRangeValidationResult(
84
- value: RangeValue<DateValue>,
85
- minValue: DateValue,
86
- maxValue: DateValue,
87
- isDateUnavailable: (v: DateValue) => boolean,
84
+ value: RangeValue<DateValue | null> | null,
85
+ minValue: DateValue | null | undefined,
86
+ maxValue: DateValue | null | undefined,
87
+ isDateUnavailable: ((v: DateValue) => boolean) | undefined,
88
88
  options: FormatterOptions
89
89
  ) {
90
90
  let startValidation = getValidationResult(
91
- value?.start,
91
+ value?.start ?? null,
92
92
  minValue,
93
93
  maxValue,
94
94
  isDateUnavailable,
@@ -96,7 +96,7 @@ export function getRangeValidationResult(
96
96
  );
97
97
 
98
98
  let endValidation = getValidationResult(
99
- value?.end,
99
+ value?.end ?? null,
100
100
  minValue,
101
101
  maxValue,
102
102
  isDateUnavailable,
@@ -104,7 +104,7 @@ export function getRangeValidationResult(
104
104
  );
105
105
 
106
106
  let result = mergeValidation(startValidation, endValidation);
107
- if (value.end != null && value.start != null && value.end.compare(value.start) < 0) {
107
+ if (value?.end != null && value.start != null && value.end.compare(value.start) < 0) {
108
108
  let strings = LocalizedStringDictionary.getGlobalDictionaryForPackage('@react-stately/datepicker') || dictionary;
109
109
  result = mergeValidation(result, {
110
110
  isInvalid: true,
@@ -195,7 +195,7 @@ export function getFormatOptions(
195
195
  return opts;
196
196
  }
197
197
 
198
- export function getPlaceholderTime(placeholderValue: DateValue): TimeValue {
198
+ export function getPlaceholderTime(placeholderValue: DateValue | null | undefined): TimeValue {
199
199
  if (placeholderValue && 'hour' in placeholderValue) {
200
200
  return placeholderValue;
201
201
  }
@@ -203,7 +203,7 @@ export function getPlaceholderTime(placeholderValue: DateValue): TimeValue {
203
203
  return new Time();
204
204
  }
205
205
 
206
- export function convertValue(value: DateValue, calendar: Calendar): DateValue {
206
+ export function convertValue(value: DateValue | null | undefined, calendar: Calendar): DateValue | null | undefined {
207
207
  if (value === null) {
208
208
  return null;
209
209
  }
@@ -216,12 +216,12 @@ export function convertValue(value: DateValue, calendar: Calendar): DateValue {
216
216
  }
217
217
 
218
218
 
219
- export function createPlaceholderDate(placeholderValue: DateValue, granularity: string, calendar: Calendar, timeZone: string) {
219
+ export function createPlaceholderDate(placeholderValue: DateValue | null | undefined, granularity: string, calendar: Calendar, timeZone: string | undefined): DateValue {
220
220
  if (placeholderValue) {
221
- return convertValue(placeholderValue, calendar);
221
+ return convertValue(placeholderValue, calendar)!;
222
222
  }
223
223
 
224
- let date = toCalendar(now(timeZone).set({
224
+ let date = toCalendar(now(timeZone ?? getLocalTimeZone()).set({
225
225
  hour: 0,
226
226
  minute: 0,
227
227
  second: 0,
@@ -239,7 +239,7 @@ export function createPlaceholderDate(placeholderValue: DateValue, granularity:
239
239
  return date;
240
240
  }
241
241
 
242
- export function useDefaultProps(v: DateValue, granularity: Granularity): [Granularity, string] {
242
+ export function useDefaultProps(v: DateValue | null, granularity: Granularity | undefined): [Granularity, string | undefined] {
243
243
  // Compute default granularity and time zone from the value. If the value becomes null, keep the last values.
244
244
  let defaultTimeZone = (v && 'timeZone' in v ? v.timeZone : undefined);
245
245
  let defaultGranularity: Granularity = (v && 'minute' in v ? 'minute' : 'day');
@@ -249,7 +249,7 @@ export function useDefaultProps(v: DateValue, granularity: Granularity): [Granul
249
249
  throw new Error('Invalid granularity ' + granularity + ' for value ' + v.toString());
250
250
  }
251
251
 
252
- let [lastValue, setLastValue] = useState<[Granularity, string]>([defaultGranularity, defaultTimeZone]);
252
+ let [lastValue, setLastValue] = useState<[Granularity, string | undefined]>([defaultGranularity, defaultTimeZone]);
253
253
 
254
254
  // If the granularity or time zone changed, update the last value.
255
255
  if (v && (lastValue[0] !== defaultGranularity || lastValue[1] !== defaultTimeZone)) {