@react-stately/datepicker 3.8.0 → 3.9.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.
Files changed (183) hide show
  1. package/dist/ar-AE.main.js +9 -0
  2. package/dist/ar-AE.main.js.map +1 -0
  3. package/dist/ar-AE.mjs +11 -0
  4. package/dist/ar-AE.module.js +11 -0
  5. package/dist/ar-AE.module.js.map +1 -0
  6. package/dist/bg-BG.main.js +9 -0
  7. package/dist/bg-BG.main.js.map +1 -0
  8. package/dist/bg-BG.mjs +11 -0
  9. package/dist/bg-BG.module.js +11 -0
  10. package/dist/bg-BG.module.js.map +1 -0
  11. package/dist/cs-CZ.main.js +9 -0
  12. package/dist/cs-CZ.main.js.map +1 -0
  13. package/dist/cs-CZ.mjs +11 -0
  14. package/dist/cs-CZ.module.js +11 -0
  15. package/dist/cs-CZ.module.js.map +1 -0
  16. package/dist/da-DK.main.js +9 -0
  17. package/dist/da-DK.main.js.map +1 -0
  18. package/dist/da-DK.mjs +11 -0
  19. package/dist/da-DK.module.js +11 -0
  20. package/dist/da-DK.module.js.map +1 -0
  21. package/dist/de-DE.main.js +9 -0
  22. package/dist/de-DE.main.js.map +1 -0
  23. package/dist/de-DE.mjs +11 -0
  24. package/dist/de-DE.module.js +11 -0
  25. package/dist/de-DE.module.js.map +1 -0
  26. package/dist/el-GR.main.js +9 -0
  27. package/dist/el-GR.main.js.map +1 -0
  28. package/dist/el-GR.mjs +11 -0
  29. package/dist/el-GR.module.js +11 -0
  30. package/dist/el-GR.module.js.map +1 -0
  31. package/dist/en-US.main.js +9 -0
  32. package/dist/en-US.main.js.map +1 -0
  33. package/dist/en-US.mjs +11 -0
  34. package/dist/en-US.module.js +11 -0
  35. package/dist/en-US.module.js.map +1 -0
  36. package/dist/es-ES.main.js +9 -0
  37. package/dist/es-ES.main.js.map +1 -0
  38. package/dist/es-ES.mjs +11 -0
  39. package/dist/es-ES.module.js +11 -0
  40. package/dist/es-ES.module.js.map +1 -0
  41. package/dist/et-EE.main.js +9 -0
  42. package/dist/et-EE.main.js.map +1 -0
  43. package/dist/et-EE.mjs +11 -0
  44. package/dist/et-EE.module.js +11 -0
  45. package/dist/et-EE.module.js.map +1 -0
  46. package/dist/fi-FI.main.js +9 -0
  47. package/dist/fi-FI.main.js.map +1 -0
  48. package/dist/fi-FI.mjs +11 -0
  49. package/dist/fi-FI.module.js +11 -0
  50. package/dist/fi-FI.module.js.map +1 -0
  51. package/dist/fr-FR.main.js +9 -0
  52. package/dist/fr-FR.main.js.map +1 -0
  53. package/dist/fr-FR.mjs +11 -0
  54. package/dist/fr-FR.module.js +11 -0
  55. package/dist/fr-FR.module.js.map +1 -0
  56. package/dist/he-IL.main.js +9 -0
  57. package/dist/he-IL.main.js.map +1 -0
  58. package/dist/he-IL.mjs +11 -0
  59. package/dist/he-IL.module.js +11 -0
  60. package/dist/he-IL.module.js.map +1 -0
  61. package/dist/hr-HR.main.js +9 -0
  62. package/dist/hr-HR.main.js.map +1 -0
  63. package/dist/hr-HR.mjs +11 -0
  64. package/dist/hr-HR.module.js +11 -0
  65. package/dist/hr-HR.module.js.map +1 -0
  66. package/dist/hu-HU.main.js +9 -0
  67. package/dist/hu-HU.main.js.map +1 -0
  68. package/dist/hu-HU.mjs +11 -0
  69. package/dist/hu-HU.module.js +11 -0
  70. package/dist/hu-HU.module.js.map +1 -0
  71. package/dist/import.mjs +361 -98
  72. package/dist/it-IT.main.js +9 -0
  73. package/dist/it-IT.main.js.map +1 -0
  74. package/dist/it-IT.mjs +11 -0
  75. package/dist/it-IT.module.js +11 -0
  76. package/dist/it-IT.module.js.map +1 -0
  77. package/dist/ja-JP.main.js +9 -0
  78. package/dist/ja-JP.main.js.map +1 -0
  79. package/dist/ja-JP.mjs +11 -0
  80. package/dist/ja-JP.module.js +11 -0
  81. package/dist/ja-JP.module.js.map +1 -0
  82. package/dist/ko-KR.main.js +9 -0
  83. package/dist/ko-KR.main.js.map +1 -0
  84. package/dist/ko-KR.mjs +11 -0
  85. package/dist/ko-KR.module.js +11 -0
  86. package/dist/ko-KR.module.js.map +1 -0
  87. package/dist/lt-LT.main.js +9 -0
  88. package/dist/lt-LT.main.js.map +1 -0
  89. package/dist/lt-LT.mjs +11 -0
  90. package/dist/lt-LT.module.js +11 -0
  91. package/dist/lt-LT.module.js.map +1 -0
  92. package/dist/lv-LV.main.js +9 -0
  93. package/dist/lv-LV.main.js.map +1 -0
  94. package/dist/lv-LV.mjs +11 -0
  95. package/dist/lv-LV.module.js +11 -0
  96. package/dist/lv-LV.module.js.map +1 -0
  97. package/dist/main.js +360 -96
  98. package/dist/main.js.map +1 -1
  99. package/dist/module.js +361 -98
  100. package/dist/module.js.map +1 -1
  101. package/dist/nb-NO.main.js +9 -0
  102. package/dist/nb-NO.main.js.map +1 -0
  103. package/dist/nb-NO.mjs +11 -0
  104. package/dist/nb-NO.module.js +11 -0
  105. package/dist/nb-NO.module.js.map +1 -0
  106. package/dist/nl-NL.main.js +9 -0
  107. package/dist/nl-NL.main.js.map +1 -0
  108. package/dist/nl-NL.mjs +11 -0
  109. package/dist/nl-NL.module.js +11 -0
  110. package/dist/nl-NL.module.js.map +1 -0
  111. package/dist/pl-PL.main.js +9 -0
  112. package/dist/pl-PL.main.js.map +1 -0
  113. package/dist/pl-PL.mjs +11 -0
  114. package/dist/pl-PL.module.js +11 -0
  115. package/dist/pl-PL.module.js.map +1 -0
  116. package/dist/pt-BR.main.js +9 -0
  117. package/dist/pt-BR.main.js.map +1 -0
  118. package/dist/pt-BR.mjs +11 -0
  119. package/dist/pt-BR.module.js +11 -0
  120. package/dist/pt-BR.module.js.map +1 -0
  121. package/dist/pt-PT.main.js +9 -0
  122. package/dist/pt-PT.main.js.map +1 -0
  123. package/dist/pt-PT.mjs +11 -0
  124. package/dist/pt-PT.module.js +11 -0
  125. package/dist/pt-PT.module.js.map +1 -0
  126. package/dist/ro-RO.main.js +9 -0
  127. package/dist/ro-RO.main.js.map +1 -0
  128. package/dist/ro-RO.mjs +11 -0
  129. package/dist/ro-RO.module.js +11 -0
  130. package/dist/ro-RO.module.js.map +1 -0
  131. package/dist/ru-RU.main.js +9 -0
  132. package/dist/ru-RU.main.js.map +1 -0
  133. package/dist/ru-RU.mjs +11 -0
  134. package/dist/ru-RU.module.js +11 -0
  135. package/dist/ru-RU.module.js.map +1 -0
  136. package/dist/sk-SK.main.js +9 -0
  137. package/dist/sk-SK.main.js.map +1 -0
  138. package/dist/sk-SK.mjs +11 -0
  139. package/dist/sk-SK.module.js +11 -0
  140. package/dist/sk-SK.module.js.map +1 -0
  141. package/dist/sl-SI.main.js +9 -0
  142. package/dist/sl-SI.main.js.map +1 -0
  143. package/dist/sl-SI.mjs +11 -0
  144. package/dist/sl-SI.module.js +11 -0
  145. package/dist/sl-SI.module.js.map +1 -0
  146. package/dist/sr-SP.main.js +9 -0
  147. package/dist/sr-SP.main.js.map +1 -0
  148. package/dist/sr-SP.mjs +11 -0
  149. package/dist/sr-SP.module.js +11 -0
  150. package/dist/sr-SP.module.js.map +1 -0
  151. package/dist/sv-SE.main.js +9 -0
  152. package/dist/sv-SE.main.js.map +1 -0
  153. package/dist/sv-SE.mjs +11 -0
  154. package/dist/sv-SE.module.js +11 -0
  155. package/dist/sv-SE.module.js.map +1 -0
  156. package/dist/tr-TR.main.js +9 -0
  157. package/dist/tr-TR.main.js.map +1 -0
  158. package/dist/tr-TR.mjs +11 -0
  159. package/dist/tr-TR.module.js +11 -0
  160. package/dist/tr-TR.module.js.map +1 -0
  161. package/dist/types.d.ts +4 -3
  162. package/dist/types.d.ts.map +1 -1
  163. package/dist/uk-UA.main.js +9 -0
  164. package/dist/uk-UA.main.js.map +1 -0
  165. package/dist/uk-UA.mjs +11 -0
  166. package/dist/uk-UA.module.js +11 -0
  167. package/dist/uk-UA.module.js.map +1 -0
  168. package/dist/zh-CN.main.js +9 -0
  169. package/dist/zh-CN.main.js.map +1 -0
  170. package/dist/zh-CN.mjs +11 -0
  171. package/dist/zh-CN.module.js +11 -0
  172. package/dist/zh-CN.module.js.map +1 -0
  173. package/dist/zh-TW.main.js +9 -0
  174. package/dist/zh-TW.main.js.map +1 -0
  175. package/dist/zh-TW.mjs +11 -0
  176. package/dist/zh-TW.module.js +11 -0
  177. package/dist/zh-TW.module.js.map +1 -0
  178. package/package.json +9 -8
  179. package/src/useDateFieldState.ts +23 -5
  180. package/src/useDatePickerState.ts +36 -16
  181. package/src/useDateRangePickerState.ts +35 -12
  182. package/src/useTimeFieldState.ts +5 -3
  183. package/src/utils.ts +104 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-stately/datepicker",
3
- "version": "3.8.0",
3
+ "version": "3.9.1",
4
4
  "description": "Spectrum UI components in React",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/main.js",
@@ -22,12 +22,13 @@
22
22
  "url": "https://github.com/adobe/react-spectrum"
23
23
  },
24
24
  "dependencies": {
25
- "@internationalized/date": "^3.5.0",
26
- "@internationalized/string": "^3.1.1",
27
- "@react-stately/overlays": "^3.6.3",
28
- "@react-stately/utils": "^3.8.0",
29
- "@react-types/datepicker": "^3.6.1",
30
- "@react-types/shared": "^3.21.0",
25
+ "@internationalized/date": "^3.5.1",
26
+ "@internationalized/string": "^3.2.0",
27
+ "@react-stately/form": "^3.0.0",
28
+ "@react-stately/overlays": "^3.6.4",
29
+ "@react-stately/utils": "^3.9.0",
30
+ "@react-types/datepicker": "^3.7.1",
31
+ "@react-types/shared": "^3.22.0",
31
32
  "@swc/helpers": "^0.5.0"
32
33
  },
33
34
  "peerDependencies": {
@@ -36,5 +37,5 @@
36
37
  "publishConfig": {
37
38
  "access": "public"
38
39
  },
39
- "gitHead": "4122e44d1991c90507d630d35ed297f89db435d3"
40
+ "gitHead": "86b38c87868ce7f262e0df905e5ac4eb2653791d"
40
41
  }
@@ -11,8 +11,9 @@
11
11
  */
12
12
 
13
13
  import {Calendar, DateFormatter, getMinimumDayInMonth, getMinimumMonthInYear, GregorianCalendar, toCalendar} from '@internationalized/date';
14
- import {convertValue, createPlaceholderDate, FieldOptions, getFormatOptions, isInvalid, useDefaultProps} from './utils';
14
+ import {convertValue, createPlaceholderDate, FieldOptions, getFormatOptions, getValidationResult, useDefaultProps} from './utils';
15
15
  import {DatePickerProps, DateValue, Granularity} from '@react-types/datepicker';
16
+ import {FormValidationState, useFormValidationState} from '@react-stately/form';
16
17
  import {getPlaceholder} from './placeholders';
17
18
  import {useControlledState} from '@react-stately/utils';
18
19
  import {useEffect, useMemo, useRef, useState} from 'react';
@@ -38,7 +39,7 @@ export interface DateSegment {
38
39
  isEditable: boolean
39
40
  }
40
41
 
41
- export interface DateFieldState {
42
+ export interface DateFieldState extends FormValidationState {
42
43
  /** The current field value. */
43
44
  value: DateValue,
44
45
  /** The current value, converted to a native JavaScript `Date` object. */
@@ -149,7 +150,10 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
149
150
  hideTimeZone,
150
151
  isDisabled,
151
152
  isReadOnly,
152
- isRequired
153
+ isRequired,
154
+ minValue,
155
+ maxValue,
156
+ isDateUnavailable
153
157
  } = props;
154
158
 
155
159
  let v: DateValue = (props.value || props.defaultValue || props.placeholderValue);
@@ -314,11 +318,25 @@ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFi
314
318
  }
315
319
  };
316
320
 
317
- let isValueInvalid = props.isInvalid || props.validationState === 'invalid' ||
318
- isInvalid(calendarValue, props.minValue, props.maxValue);
321
+ let builtinValidation = useMemo(() => getValidationResult(
322
+ value,
323
+ minValue,
324
+ maxValue,
325
+ isDateUnavailable,
326
+ formatOpts
327
+ ), [value, minValue, maxValue, isDateUnavailable, formatOpts]);
328
+
329
+ let validation = useFormValidationState({
330
+ ...props,
331
+ value,
332
+ builtinValidation
333
+ });
334
+
335
+ let isValueInvalid = validation.displayValidation.isInvalid;
319
336
  let validationState: ValidationState = props.validationState || (isValueInvalid ? 'invalid' : null);
320
337
 
321
338
  return {
339
+ ...validation,
322
340
  value: calendarValue,
323
341
  dateValue,
324
342
  calendar,
@@ -12,10 +12,11 @@
12
12
 
13
13
  import {CalendarDate, DateFormatter, toCalendarDate, toCalendarDateTime} from '@internationalized/date';
14
14
  import {DatePickerProps, DateValue, Granularity, TimeValue} from '@react-types/datepicker';
15
- import {FieldOptions, getFormatOptions, getPlaceholderTime, isInvalid, useDefaultProps} from './utils';
15
+ import {FieldOptions, getFormatOptions, getPlaceholderTime, getValidationResult, useDefaultProps} from './utils';
16
+ import {FormValidationState, useFormValidationState} from '@react-stately/form';
16
17
  import {OverlayTriggerState, useOverlayTriggerState} from '@react-stately/overlays';
17
18
  import {useControlledState} from '@react-stately/utils';
18
- import {useState} from 'react';
19
+ import {useMemo, useState} from 'react';
19
20
  import {ValidationState} from '@react-types/shared';
20
21
 
21
22
  export interface DatePickerStateOptions<T extends DateValue> extends DatePickerProps<T> {
@@ -26,7 +27,7 @@ export interface DatePickerStateOptions<T extends DateValue> extends DatePickerP
26
27
  shouldCloseOnSelect?: boolean | (() => boolean)
27
28
  }
28
29
 
29
- export interface DatePickerState extends OverlayTriggerState {
30
+ export interface DatePickerState extends OverlayTriggerState, FormValidationState {
30
31
  /** The currently selected date. */
31
32
  value: DateValue | null,
32
33
  /** Sets the selected date. */
@@ -93,10 +94,39 @@ export function useDatePickerState<T extends DateValue = DateValue>(props: DateP
93
94
  throw new Error('Invalid granularity ' + granularity + ' for value ' + v.toString());
94
95
  }
95
96
 
97
+ let showEra = value?.calendar.identifier === 'gregory' && value.era === 'BC';
98
+ let formatOpts = useMemo(() => ({
99
+ granularity,
100
+ timeZone: defaultTimeZone,
101
+ hideTimeZone: props.hideTimeZone,
102
+ hourCycle: props.hourCycle,
103
+ shouldForceLeadingZeros: props.shouldForceLeadingZeros,
104
+ showEra
105
+ }), [granularity, props.hourCycle, props.shouldForceLeadingZeros, defaultTimeZone, props.hideTimeZone, showEra]);
106
+
107
+ let {minValue, maxValue, isDateUnavailable} = props;
108
+ let builtinValidation = useMemo(() => getValidationResult(
109
+ value,
110
+ minValue,
111
+ maxValue,
112
+ isDateUnavailable,
113
+ formatOpts
114
+ ), [value, minValue, maxValue, isDateUnavailable, formatOpts]);
115
+
116
+ let validation = useFormValidationState({
117
+ ...props,
118
+ value,
119
+ builtinValidation
120
+ });
121
+
122
+ let isValueInvalid = validation.displayValidation.isInvalid;
123
+ let validationState: ValidationState = props.validationState || (isValueInvalid ? 'invalid' : null);
124
+
96
125
  let commitValue = (date: DateValue, time: TimeValue) => {
97
126
  setValue('timeZone' in time ? time.set(toCalendarDate(date)) : toCalendarDateTime(date, time));
98
127
  setSelectedDate(null);
99
128
  setSelectedTime(null);
129
+ validation.commitValidation();
100
130
  };
101
131
 
102
132
  // Intercept setValue to make sure the Time section is not changed by date selection in Calendar
@@ -110,6 +140,7 @@ export function useDatePickerState<T extends DateValue = DateValue>(props: DateP
110
140
  }
111
141
  } else {
112
142
  setValue(newValue);
143
+ validation.commitValidation();
113
144
  }
114
145
 
115
146
  if (shouldClose) {
@@ -125,12 +156,8 @@ export function useDatePickerState<T extends DateValue = DateValue>(props: DateP
125
156
  }
126
157
  };
127
158
 
128
- let isValueInvalid = props.isInvalid || props.validationState === 'invalid' ||
129
- isInvalid(value, props.minValue, props.maxValue) ||
130
- value && props.isDateUnavailable?.(value);
131
- let validationState: ValidationState = props.validationState || (isValueInvalid ? 'invalid' : null);
132
-
133
159
  return {
160
+ ...validation,
134
161
  value,
135
162
  setValue,
136
163
  dateValue: selectedDate,
@@ -157,14 +184,7 @@ export function useDatePickerState<T extends DateValue = DateValue>(props: DateP
157
184
  return '';
158
185
  }
159
186
 
160
- let formatOptions = getFormatOptions(fieldOptions, {
161
- granularity,
162
- timeZone: defaultTimeZone,
163
- hideTimeZone: props.hideTimeZone,
164
- hourCycle: props.hourCycle,
165
- showEra: value.calendar.identifier === 'gregory' && value.era === 'BC'
166
- });
167
-
187
+ let formatOptions = getFormatOptions(fieldOptions, formatOpts);
168
188
  let formatter = new DateFormatter(locale, formatOptions);
169
189
  return formatter.format(dateValue);
170
190
  }
@@ -12,11 +12,12 @@
12
12
 
13
13
  import {DateFormatter, toCalendarDate, toCalendarDateTime} from '@internationalized/date';
14
14
  import {DateRange, DateRangePickerProps, DateValue, Granularity, TimeValue} from '@react-types/datepicker';
15
- import {FieldOptions, getFormatOptions, getPlaceholderTime, isInvalid, useDefaultProps} from './utils';
15
+ import {FieldOptions, getFormatOptions, getPlaceholderTime, getRangeValidationResult, useDefaultProps} from './utils';
16
+ import {FormValidationState, useFormValidationState} from '@react-stately/form';
16
17
  import {OverlayTriggerState, useOverlayTriggerState} from '@react-stately/overlays';
17
18
  import {RangeValue, ValidationState} from '@react-types/shared';
18
19
  import {useControlledState} from '@react-stately/utils';
19
- import {useState} from 'react';
20
+ import {useMemo, useState} from 'react';
20
21
 
21
22
  export interface DateRangePickerStateOptions<T extends DateValue = DateValue> extends DateRangePickerProps<T> {
22
23
  /**
@@ -27,7 +28,7 @@ export interface DateRangePickerStateOptions<T extends DateValue = DateValue> ex
27
28
  }
28
29
 
29
30
  type TimeRange = RangeValue<TimeValue>;
30
- export interface DateRangePickerState extends OverlayTriggerState {
31
+ export interface DateRangePickerState extends OverlayTriggerState, FormValidationState {
31
32
  /** The currently selected date range. */
32
33
  value: DateRange | null,
33
34
  /** Sets the selected date range. */
@@ -99,7 +100,7 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
99
100
  };
100
101
 
101
102
  let v = (value?.start || value?.end || props.placeholderValue);
102
- let [granularity] = useDefaultProps(v, props.granularity);
103
+ let [granularity, defaultTimeZone] = useDefaultProps(v, props.granularity);
103
104
  let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second';
104
105
  let shouldCloseOnSelect = props.shouldCloseOnSelect ?? true;
105
106
 
@@ -120,6 +121,7 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
120
121
  });
121
122
  setSelectedDateRange(null);
122
123
  setSelectedTimeRange(null);
124
+ validation.commitValidation();
123
125
  };
124
126
 
125
127
  // Intercept setValue to make sure the Time section is not changed by date selection in Calendar
@@ -136,6 +138,7 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
136
138
  }
137
139
  } else if (range.start && range.end) {
138
140
  setValue(range);
141
+ validation.commitValidation();
139
142
  } else {
140
143
  setSelectedDateRange(range);
141
144
  }
@@ -153,17 +156,37 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
153
156
  }
154
157
  };
155
158
 
156
- let isValueInvalid = props.isInvalid || props.validationState === 'invalid'
157
- || (value != null && (
158
- isInvalid(value.start, props.minValue, props.maxValue) ||
159
- isInvalid(value.end, props.minValue, props.maxValue) ||
160
- (value.end != null && value.start != null && value.end.compare(value.start) < 0) ||
161
- (value?.start && props.isDateUnavailable?.(value.start)) ||
162
- (value?.end && props.isDateUnavailable?.(value.end))
163
- ));
159
+ let showEra = (value?.start?.calendar.identifier === 'gregory' && value.start.era === 'BC') || (value?.end?.calendar.identifier === 'gregory' && value.end.era === 'BC');
160
+ let formatOpts = useMemo(() => ({
161
+ granularity,
162
+ timeZone: defaultTimeZone,
163
+ hideTimeZone: props.hideTimeZone,
164
+ hourCycle: props.hourCycle,
165
+ shouldForceLeadingZeros: props.shouldForceLeadingZeros,
166
+ showEra
167
+ }), [granularity, props.hourCycle, props.shouldForceLeadingZeros, defaultTimeZone, props.hideTimeZone, showEra]);
168
+
169
+ let {minValue, maxValue, isDateUnavailable} = props;
170
+ let builtinValidation = useMemo(() => getRangeValidationResult(
171
+ value,
172
+ minValue,
173
+ maxValue,
174
+ isDateUnavailable,
175
+ formatOpts
176
+ ), [value, minValue, maxValue, isDateUnavailable, formatOpts]);
177
+
178
+ let validation = useFormValidationState({
179
+ ...props,
180
+ value: controlledValue,
181
+ name: useMemo(() => [props.startName, props.endName], [props.startName, props.endName]),
182
+ builtinValidation
183
+ });
184
+
185
+ let isValueInvalid = validation.displayValidation.isInvalid;
164
186
  let validationState: ValidationState = props.validationState || (isValueInvalid ? 'invalid' : null);
165
187
 
166
188
  return {
189
+ ...validation,
167
190
  value,
168
191
  setValue,
169
192
  dateRange,
@@ -13,8 +13,8 @@
13
13
  import {DateFieldState, useDateFieldState} from '.';
14
14
  import {DateValue, TimePickerProps, TimeValue} from '@react-types/datepicker';
15
15
  import {getLocalTimeZone, GregorianCalendar, Time, toCalendarDateTime, today, toTime, toZoned} from '@internationalized/date';
16
+ import {useCallback, useMemo} from 'react';
16
17
  import {useControlledState} from '@react-stately/utils';
17
- import {useMemo} from 'react';
18
18
 
19
19
  export interface TimeFieldStateOptions<T extends TimeValue = TimeValue> extends TimePickerProps<T> {
20
20
  /** The locale to display and edit the value according to. */
@@ -36,7 +36,8 @@ export function useTimeFieldState<T extends TimeValue = TimeValue>(props: TimeFi
36
36
  placeholderValue = new Time(),
37
37
  minValue,
38
38
  maxValue,
39
- granularity
39
+ granularity,
40
+ validate
40
41
  } = props;
41
42
 
42
43
  let [value, setValue] = useControlledState<TimeValue>(
@@ -73,7 +74,8 @@ export function useTimeFieldState<T extends TimeValue = TimeValue>(props: TimeFi
73
74
  maxGranularity: 'hour',
74
75
  placeholderValue: placeholderDate,
75
76
  // Calendar should not matter for time fields.
76
- createCalendar: () => new GregorianCalendar()
77
+ createCalendar: () => new GregorianCalendar(),
78
+ validate: useCallback(() => validate?.(value as any), [validate, value])
77
79
  });
78
80
 
79
81
  return {
package/src/utils.ts CHANGED
@@ -10,15 +10,114 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {Calendar, now, Time, toCalendar, toCalendarDate, toCalendarDateTime} from '@internationalized/date';
13
+ import {Calendar, DateFormatter, now, Time, toCalendar, toCalendarDate, toCalendarDateTime} from '@internationalized/date';
14
14
  import {DatePickerProps, DateValue, Granularity, TimeValue} from '@react-types/datepicker';
15
+ // @ts-ignore
16
+ import i18nMessages from '../intl/*.json';
17
+ import {LocalizedStringDictionary, LocalizedStringFormatter} from '@internationalized/string';
18
+ import {mergeValidation, VALID_VALIDITY_STATE} from '@react-stately/form';
19
+ import {RangeValue, ValidationResult} from '@react-types/shared';
15
20
  import {useState} from 'react';
16
21
 
17
- export function isInvalid(value: DateValue, minValue: DateValue, maxValue: DateValue) {
18
- return value != null && (
19
- (minValue != null && value.compare(minValue) < 0) ||
20
- (maxValue != null && value.compare(maxValue) > 0)
22
+ const dictionary = new LocalizedStringDictionary(i18nMessages);
23
+
24
+ function getLocale() {
25
+ // Match browser language setting here, NOT react-aria's I18nProvider, so that we match other browser-provided
26
+ // validation messages, which to not respect our provider's language.
27
+ // @ts-ignore
28
+ return (typeof navigator !== 'undefined' && (navigator.language || navigator.userLanguage)) || 'en-US';
29
+ }
30
+
31
+ export function getValidationResult(
32
+ value: DateValue,
33
+ minValue: DateValue,
34
+ maxValue: DateValue,
35
+ isDateUnavailable: (v: DateValue) => boolean,
36
+ options: FormatterOptions
37
+ ): ValidationResult {
38
+ let rangeOverflow = value != null && maxValue != null && value.compare(maxValue) > 0;
39
+ let rangeUnderflow = value != null && minValue != null && value.compare(minValue) < 0;
40
+ let isUnavailable = (value != null && isDateUnavailable?.(value)) || false;
41
+ let isInvalid = rangeOverflow || rangeUnderflow || isUnavailable;
42
+ let errors = [];
43
+
44
+ if (isInvalid) {
45
+ let locale = getLocale();
46
+ let strings = LocalizedStringDictionary.getGlobalDictionaryForPackage('@react-stately/datepicker') || dictionary;
47
+ let formatter = new LocalizedStringFormatter(locale, strings);
48
+ let dateFormatter = new DateFormatter(locale, getFormatOptions({}, options));
49
+ let timeZone = dateFormatter.resolvedOptions().timeZone;
50
+
51
+ if (rangeUnderflow) {
52
+ errors.push(formatter.format('rangeUnderflow', {minValue: dateFormatter.format(minValue.toDate(timeZone))}));
53
+ }
54
+
55
+ if (rangeOverflow) {
56
+ errors.push(formatter.format('rangeOverflow', {maxValue: dateFormatter.format(maxValue.toDate(timeZone))}));
57
+ }
58
+
59
+ if (isUnavailable) {
60
+ errors.push(formatter.format('unavailableDate'));
61
+ }
62
+ }
63
+
64
+ return {
65
+ isInvalid,
66
+ validationErrors: errors,
67
+ validationDetails: {
68
+ badInput: isUnavailable,
69
+ customError: false,
70
+ patternMismatch: false,
71
+ rangeOverflow,
72
+ rangeUnderflow,
73
+ stepMismatch: false,
74
+ tooLong: false,
75
+ tooShort: false,
76
+ typeMismatch: false,
77
+ valueMissing: false,
78
+ valid: !isInvalid
79
+ }
80
+ };
81
+ }
82
+
83
+ export function getRangeValidationResult(
84
+ value: RangeValue<DateValue>,
85
+ minValue: DateValue,
86
+ maxValue: DateValue,
87
+ isDateUnavailable: (v: DateValue) => boolean,
88
+ options: FormatterOptions
89
+ ) {
90
+ let startValidation = getValidationResult(
91
+ value?.start,
92
+ minValue,
93
+ maxValue,
94
+ isDateUnavailable,
95
+ options
21
96
  );
97
+
98
+ let endValidation = getValidationResult(
99
+ value?.end,
100
+ minValue,
101
+ maxValue,
102
+ isDateUnavailable,
103
+ options
104
+ );
105
+
106
+ let result = mergeValidation(startValidation, endValidation);
107
+ if (value.end != null && value.start != null && value.end.compare(value.start) < 0) {
108
+ result = mergeValidation(result, {
109
+ isInvalid: true,
110
+ validationErrors: [dictionary.getStringForLocale('rangeReversed', getLocale())],
111
+ validationDetails: {
112
+ ...VALID_VALIDITY_STATE,
113
+ rangeUnderflow: true,
114
+ rangeOverflow: true,
115
+ valid: false
116
+ }
117
+ });
118
+ }
119
+
120
+ return result;
22
121
  }
23
122
 
24
123
  export type FieldOptions = Pick<Intl.DateTimeFormatOptions, 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'>;