@react-stately/datepicker 3.0.0-alpha.4 → 3.0.0-nightly-4980928d3-240906

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 (221) 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 +23 -0
  72. package/dist/intlStrings.main.js +108 -0
  73. package/dist/intlStrings.main.js.map +1 -0
  74. package/dist/intlStrings.mjs +110 -0
  75. package/dist/intlStrings.module.js +110 -0
  76. package/dist/intlStrings.module.js.map +1 -0
  77. package/dist/it-IT.main.js +9 -0
  78. package/dist/it-IT.main.js.map +1 -0
  79. package/dist/it-IT.mjs +11 -0
  80. package/dist/it-IT.module.js +11 -0
  81. package/dist/it-IT.module.js.map +1 -0
  82. package/dist/ja-JP.main.js +9 -0
  83. package/dist/ja-JP.main.js.map +1 -0
  84. package/dist/ja-JP.mjs +11 -0
  85. package/dist/ja-JP.module.js +11 -0
  86. package/dist/ja-JP.module.js.map +1 -0
  87. package/dist/ko-KR.main.js +9 -0
  88. package/dist/ko-KR.main.js.map +1 -0
  89. package/dist/ko-KR.mjs +11 -0
  90. package/dist/ko-KR.module.js +11 -0
  91. package/dist/ko-KR.module.js.map +1 -0
  92. package/dist/lt-LT.main.js +9 -0
  93. package/dist/lt-LT.main.js.map +1 -0
  94. package/dist/lt-LT.mjs +11 -0
  95. package/dist/lt-LT.module.js +11 -0
  96. package/dist/lt-LT.module.js.map +1 -0
  97. package/dist/lv-LV.main.js +9 -0
  98. package/dist/lv-LV.main.js.map +1 -0
  99. package/dist/lv-LV.mjs +11 -0
  100. package/dist/lv-LV.module.js +11 -0
  101. package/dist/lv-LV.module.js.map +1 -0
  102. package/dist/main.js +19 -765
  103. package/dist/main.js.map +1 -1
  104. package/dist/module.js +16 -751
  105. package/dist/module.js.map +1 -1
  106. package/dist/nb-NO.main.js +9 -0
  107. package/dist/nb-NO.main.js.map +1 -0
  108. package/dist/nb-NO.mjs +11 -0
  109. package/dist/nb-NO.module.js +11 -0
  110. package/dist/nb-NO.module.js.map +1 -0
  111. package/dist/nl-NL.main.js +9 -0
  112. package/dist/nl-NL.main.js.map +1 -0
  113. package/dist/nl-NL.mjs +11 -0
  114. package/dist/nl-NL.module.js +11 -0
  115. package/dist/nl-NL.module.js.map +1 -0
  116. package/dist/pl-PL.main.js +9 -0
  117. package/dist/pl-PL.main.js.map +1 -0
  118. package/dist/pl-PL.mjs +11 -0
  119. package/dist/pl-PL.module.js +11 -0
  120. package/dist/pl-PL.module.js.map +1 -0
  121. package/dist/placeholders.main.js +409 -0
  122. package/dist/placeholders.main.js.map +1 -0
  123. package/dist/placeholders.mjs +404 -0
  124. package/dist/placeholders.module.js +404 -0
  125. package/dist/placeholders.module.js.map +1 -0
  126. package/dist/pt-BR.main.js +9 -0
  127. package/dist/pt-BR.main.js.map +1 -0
  128. package/dist/pt-BR.mjs +11 -0
  129. package/dist/pt-BR.module.js +11 -0
  130. package/dist/pt-BR.module.js.map +1 -0
  131. package/dist/pt-PT.main.js +9 -0
  132. package/dist/pt-PT.main.js.map +1 -0
  133. package/dist/pt-PT.mjs +11 -0
  134. package/dist/pt-PT.module.js +11 -0
  135. package/dist/pt-PT.module.js.map +1 -0
  136. package/dist/ro-RO.main.js +9 -0
  137. package/dist/ro-RO.main.js.map +1 -0
  138. package/dist/ro-RO.mjs +11 -0
  139. package/dist/ro-RO.module.js +11 -0
  140. package/dist/ro-RO.module.js.map +1 -0
  141. package/dist/ru-RU.main.js +9 -0
  142. package/dist/ru-RU.main.js.map +1 -0
  143. package/dist/ru-RU.mjs +11 -0
  144. package/dist/ru-RU.module.js +11 -0
  145. package/dist/ru-RU.module.js.map +1 -0
  146. package/dist/sk-SK.main.js +9 -0
  147. package/dist/sk-SK.main.js.map +1 -0
  148. package/dist/sk-SK.mjs +11 -0
  149. package/dist/sk-SK.module.js +11 -0
  150. package/dist/sk-SK.module.js.map +1 -0
  151. package/dist/sl-SI.main.js +9 -0
  152. package/dist/sl-SI.main.js.map +1 -0
  153. package/dist/sl-SI.mjs +11 -0
  154. package/dist/sl-SI.module.js +11 -0
  155. package/dist/sl-SI.module.js.map +1 -0
  156. package/dist/sr-SP.main.js +9 -0
  157. package/dist/sr-SP.main.js.map +1 -0
  158. package/dist/sr-SP.mjs +11 -0
  159. package/dist/sr-SP.module.js +11 -0
  160. package/dist/sr-SP.module.js.map +1 -0
  161. package/dist/sv-SE.main.js +9 -0
  162. package/dist/sv-SE.main.js.map +1 -0
  163. package/dist/sv-SE.mjs +11 -0
  164. package/dist/sv-SE.module.js +11 -0
  165. package/dist/sv-SE.module.js.map +1 -0
  166. package/dist/tr-TR.main.js +9 -0
  167. package/dist/tr-TR.main.js.map +1 -0
  168. package/dist/tr-TR.mjs +11 -0
  169. package/dist/tr-TR.module.js +11 -0
  170. package/dist/tr-TR.module.js.map +1 -0
  171. package/dist/types.d.ts +70 -29
  172. package/dist/types.d.ts.map +1 -1
  173. package/dist/uk-UA.main.js +9 -0
  174. package/dist/uk-UA.main.js.map +1 -0
  175. package/dist/uk-UA.mjs +11 -0
  176. package/dist/uk-UA.module.js +11 -0
  177. package/dist/uk-UA.module.js.map +1 -0
  178. package/dist/useDateFieldState.main.js +449 -0
  179. package/dist/useDateFieldState.main.js.map +1 -0
  180. package/dist/useDateFieldState.mjs +444 -0
  181. package/dist/useDateFieldState.module.js +444 -0
  182. package/dist/useDateFieldState.module.js.map +1 -0
  183. package/dist/useDatePickerState.main.js +138 -0
  184. package/dist/useDatePickerState.main.js.map +1 -0
  185. package/dist/useDatePickerState.mjs +133 -0
  186. package/dist/useDatePickerState.module.js +133 -0
  187. package/dist/useDatePickerState.module.js.map +1 -0
  188. package/dist/useDateRangePickerState.main.js +245 -0
  189. package/dist/useDateRangePickerState.main.js.map +1 -0
  190. package/dist/useDateRangePickerState.mjs +240 -0
  191. package/dist/useDateRangePickerState.module.js +240 -0
  192. package/dist/useDateRangePickerState.module.js.map +1 -0
  193. package/dist/useTimeFieldState.main.js +86 -0
  194. package/dist/useTimeFieldState.main.js.map +1 -0
  195. package/dist/useTimeFieldState.mjs +81 -0
  196. package/dist/useTimeFieldState.module.js +81 -0
  197. package/dist/useTimeFieldState.module.js.map +1 -0
  198. package/dist/utils.main.js +190 -0
  199. package/dist/utils.main.js.map +1 -0
  200. package/dist/utils.mjs +179 -0
  201. package/dist/utils.module.js +179 -0
  202. package/dist/utils.module.js.map +1 -0
  203. package/dist/zh-CN.main.js +9 -0
  204. package/dist/zh-CN.main.js.map +1 -0
  205. package/dist/zh-CN.mjs +11 -0
  206. package/dist/zh-CN.module.js +11 -0
  207. package/dist/zh-CN.module.js.map +1 -0
  208. package/dist/zh-TW.main.js +9 -0
  209. package/dist/zh-TW.main.js.map +1 -0
  210. package/dist/zh-TW.mjs +11 -0
  211. package/dist/zh-TW.module.js +11 -0
  212. package/dist/zh-TW.module.js.map +1 -0
  213. package/package.json +17 -10
  214. package/src/index.ts +9 -4
  215. package/src/placeholders.ts +108 -0
  216. package/src/useDateFieldState.ts +124 -52
  217. package/src/useDatePickerState.ts +64 -27
  218. package/src/useDateRangePickerState.ts +103 -47
  219. package/src/useTimeFieldState.ts +27 -9
  220. package/src/utils.ts +140 -18
  221. package/LICENSE +0 -201
@@ -11,8 +11,10 @@
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, FormatterOptions, getFormatOptions, getValidationResult, useDefaultProps} from './utils';
15
15
  import {DatePickerProps, DateValue, Granularity} from '@react-types/datepicker';
16
+ import {FormValidationState, useFormValidationState} from '@react-stately/form';
17
+ import {getPlaceholder} from './placeholders';
16
18
  import {useControlledState} from '@react-stately/utils';
17
19
  import {useEffect, useMemo, useRef, useState} from 'react';
18
20
  import {ValidationState} from '@react-types/shared';
@@ -31,25 +33,36 @@ export interface DateSegment {
31
33
  maxValue?: number,
32
34
  /** Whether the value is a placeholder. */
33
35
  isPlaceholder: boolean,
36
+ /** A placeholder string for the segment. */
37
+ placeholder: string,
34
38
  /** Whether the segment is editable. */
35
39
  isEditable: boolean
36
40
  }
37
41
 
38
- export interface DateFieldState {
42
+ export interface DateFieldState extends FormValidationState {
39
43
  /** The current field value. */
40
44
  value: DateValue,
41
45
  /** The current value, converted to a native JavaScript `Date` object. */
42
46
  dateValue: Date,
47
+ /** The calendar system currently in use. */
48
+ calendar: Calendar,
43
49
  /** Sets the field's value. */
44
50
  setValue(value: DateValue): void,
45
51
  /** A list of segments for the current value. */
46
52
  segments: DateSegment[],
47
53
  /** A date formatter configured for the current locale and format. */
48
54
  dateFormatter: DateFormatter,
49
- /** The current validation state of the date field, based on the `validationState`, `minValue`, and `maxValue` props. */
55
+ /**
56
+ * The current validation state of the date field, based on the `validationState`, `minValue`, and `maxValue` props.
57
+ * @deprecated Use `isInvalid` instead.
58
+ */
50
59
  validationState: ValidationState,
60
+ /** Whether the date field is invalid, based on the `isInvalid`, `minValue`, and `maxValue` props. */
61
+ isInvalid: boolean,
51
62
  /** The granularity for the field, based on the `granularity` prop and current value. */
52
63
  granularity: Granularity,
64
+ /** The maximum date or time unit that is displayed in the field. */
65
+ maxGranularity: 'year' | 'month' | Granularity,
53
66
  /** Whether the field is disabled. */
54
67
  isDisabled: boolean,
55
68
  /** Whether the field is read only. */
@@ -73,16 +86,16 @@ export interface DateFieldState {
73
86
  */
74
87
  decrementPage(type: SegmentType): void,
75
88
  /** Sets the value of the given segment. */
89
+ setSegment(type: 'era', value: string): void,
76
90
  setSegment(type: SegmentType, value: number): void,
77
- /**
78
- * Replaces the value of the date field with the placeholder value.
79
- * If a segment type is provided, only that segment is confirmed. Otherwise, all segments that have not been entered yet are confirmed.
80
- */
81
- confirmPlaceholder(type?: SegmentType): void,
91
+ /** Updates the remaining unfilled segments with the placeholder value. */
92
+ confirmPlaceholder(): void,
82
93
  /** Clears the value of the given segment, reverting it to the placeholder. */
83
94
  clearSegment(type: SegmentType): void,
84
95
  /** Formats the current date value using the given options. */
85
- formatValue(fieldOptions: FieldOptions): string
96
+ formatValue(fieldOptions: FieldOptions): string,
97
+ /** Gets a formatter based on state's props. */
98
+ getDateFormatter(locale: string, formatOptions: FormatterOptions): DateFormatter
86
99
  }
87
100
 
88
101
  const EDITABLE_SEGMENTS = {
@@ -110,7 +123,7 @@ const TYPE_MAPPING = {
110
123
  dayperiod: 'dayPeriod'
111
124
  };
112
125
 
113
- interface DatePickerFieldProps extends DatePickerProps<DateValue> {
126
+ export interface DateFieldStateOptions<T extends DateValue = DateValue> extends DatePickerProps<T> {
114
127
  /**
115
128
  * The maximum unit to display in the date field.
116
129
  * @default 'year'
@@ -132,14 +145,17 @@ interface DatePickerFieldProps extends DatePickerProps<DateValue> {
132
145
  * A date field allows users to enter and edit date and time values using a keyboard.
133
146
  * Each part of a date value is displayed in an individually editable segment.
134
147
  */
135
- export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
148
+ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFieldStateOptions<T>): DateFieldState {
136
149
  let {
137
150
  locale,
138
151
  createCalendar,
139
152
  hideTimeZone,
140
153
  isDisabled,
141
154
  isReadOnly,
142
- isRequired
155
+ isRequired,
156
+ minValue,
157
+ maxValue,
158
+ isDateUnavailable
143
159
  } = props;
144
160
 
145
161
  let v: DateValue = (props.value || props.defaultValue || props.placeholderValue);
@@ -151,22 +167,44 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
151
167
  throw new Error('Invalid granularity ' + granularity + ' for value ' + v.toString());
152
168
  }
153
169
 
170
+ let defaultFormatter = useMemo(() => new DateFormatter(locale), [locale]);
171
+ let calendar = useMemo(() => createCalendar(defaultFormatter.resolvedOptions().calendar), [createCalendar, defaultFormatter]);
172
+
173
+ let [value, setDate] = useControlledState<DateValue>(
174
+ props.value,
175
+ props.defaultValue,
176
+ props.onChange
177
+ );
178
+
179
+ let calendarValue = useMemo(() => convertValue(value, calendar), [value, calendar]);
180
+
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
+ let val = calendarValue || placeholderDate;
190
+ let showEra = calendar.identifier === 'gregory' && val.era === 'BC';
154
191
  let formatOpts = useMemo(() => ({
155
192
  granularity,
156
193
  maxGranularity: props.maxGranularity ?? 'year',
157
194
  timeZone: defaultTimeZone,
158
195
  hideTimeZone,
159
- hourCycle: props.hourCycle
160
- }), [props.maxGranularity, granularity, props.hourCycle, defaultTimeZone, hideTimeZone]);
196
+ hourCycle: props.hourCycle,
197
+ showEra,
198
+ shouldForceLeadingZeros: props.shouldForceLeadingZeros
199
+ }), [props.maxGranularity, granularity, props.hourCycle, props.shouldForceLeadingZeros, defaultTimeZone, hideTimeZone, showEra]);
161
200
  let opts = useMemo(() => getFormatOptions({}, formatOpts), [formatOpts]);
162
201
 
163
202
  let dateFormatter = useMemo(() => new DateFormatter(locale, opts), [locale, opts]);
164
203
  let resolvedOptions = useMemo(() => dateFormatter.resolvedOptions(), [dateFormatter]);
165
- let calendar = useMemo(() => createCalendar(resolvedOptions.calendar), [createCalendar, resolvedOptions.calendar]);
166
204
 
167
205
  // Determine how many editable segments there are for validation purposes.
168
206
  // The result is cached for performance.
169
- let allSegments = useMemo(() =>
207
+ let allSegments: Partial<typeof EDITABLE_SEGMENTS> = useMemo(() =>
170
208
  dateFormatter.formatToParts(new Date())
171
209
  .filter(seg => EDITABLE_SEGMENTS[seg.type])
172
210
  .reduce((p, seg) => (p[seg.type] = true, p), {})
@@ -176,13 +214,7 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
176
214
  () => props.value || props.defaultValue ? {...allSegments} : {}
177
215
  );
178
216
 
179
- // We keep track of the placeholder date separately in state so that onChange is not called
180
- // until all segments are set. If the value === null (not undefined), then assume the component
181
- // is controlled, so use the placeholder as the value until all segments are entered so it doesn't
182
- // change from uncontrolled to controlled and emit a warning.
183
- let [placeholderDate, setPlaceholderDate] = useState(
184
- () => createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone)
185
- );
217
+ let clearedSegment = useRef<string>(undefined);
186
218
 
187
219
  // Reset placeholder when calendar changes
188
220
  let lastCalendarIdentifier = useRef(calendar.identifier);
@@ -197,14 +229,6 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
197
229
  }
198
230
  }, [calendar, granularity, validSegments, defaultTimeZone, props.placeholderValue]);
199
231
 
200
- let [value, setDate] = useControlledState<DateValue>(
201
- props.value,
202
- props.defaultValue,
203
- props.onChange
204
- );
205
-
206
- let calendarValue = useMemo(() => convertValue(value, calendar), [value, calendar]);
207
-
208
232
  // If there is a value prop, and some segments were previously placeholders, mark them all as valid.
209
233
  if (value && Object.keys(validSegments).length < Object.keys(allSegments).length) {
210
234
  validSegments = {...allSegments};
@@ -224,8 +248,15 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
224
248
  if (props.isDisabled || props.isReadOnly) {
225
249
  return;
226
250
  }
251
+ let validKeys = Object.keys(validSegments);
252
+ let allKeys = Object.keys(allSegments);
227
253
 
228
- if (Object.keys(validSegments).length >= Object.keys(allSegments).length) {
254
+ // if all the segments are completed or a timefield with everything but am/pm set the time, also ignore when am/pm cleared
255
+ if (newValue == null) {
256
+ setDate(null);
257
+ setPlaceholderDate(createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone));
258
+ setValidSegments({});
259
+ } else if (validKeys.length >= allKeys.length || (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod && clearedSegment.current !== 'dayPeriod')) {
229
260
  // The display calendar should not have any effect on the emitted value.
230
261
  // Emit dates in the same calendar as the original value, if any, otherwise gregorian.
231
262
  newValue = toCalendar(newValue, v?.calendar || new GregorianCalendar());
@@ -233,6 +264,7 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
233
264
  } else {
234
265
  setPlaceholderDate(newValue);
235
266
  }
267
+ clearedSegment.current = null;
236
268
  };
237
269
 
238
270
  let dateValue = useMemo(() => displayValue.toDate(timeZone), [displayValue, timeZone]);
@@ -244,42 +276,79 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
244
276
  isEditable = false;
245
277
  }
246
278
 
279
+ let isPlaceholder = EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type];
280
+ let placeholder = EDITABLE_SEGMENTS[segment.type] ? getPlaceholder(segment.type, segment.value, locale) : null;
247
281
  return {
248
282
  type: TYPE_MAPPING[segment.type] || segment.type,
249
- text: segment.value,
283
+ text: isPlaceholder ? placeholder : segment.value,
250
284
  ...getSegmentLimits(displayValue, segment.type, resolvedOptions),
251
- isPlaceholder: EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type],
285
+ isPlaceholder,
286
+ placeholder,
252
287
  isEditable
253
288
  } as DateSegment;
254
289
  })
255
- , [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar]);
290
+ , [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale]);
256
291
 
257
- let hasEra = useMemo(() => segments.some(s => s.type === 'era'), [segments]);
292
+ // When the era field appears, mark it valid if the year field is already valid.
293
+ // If the era field disappears, remove it from the valid segments.
294
+ if (allSegments.era && validSegments.year && !validSegments.era) {
295
+ validSegments.era = true;
296
+ setValidSegments({...validSegments});
297
+ } else if (!allSegments.era && validSegments.era) {
298
+ delete validSegments.era;
299
+ setValidSegments({...validSegments});
300
+ }
258
301
 
259
302
  let markValid = (part: Intl.DateTimeFormatPartTypes) => {
260
303
  validSegments[part] = true;
261
- if (part === 'year' && hasEra) {
304
+ if (part === 'year' && allSegments.era) {
262
305
  validSegments.era = true;
263
306
  }
264
307
  setValidSegments({...validSegments});
265
308
  };
266
309
 
267
310
  let adjustSegment = (type: Intl.DateTimeFormatPartTypes, amount: number) => {
268
- markValid(type);
269
- setValue(addSegment(displayValue, type, amount, resolvedOptions));
311
+ if (!validSegments[type]) {
312
+ markValid(type);
313
+ let validKeys = Object.keys(validSegments);
314
+ let allKeys = Object.keys(allSegments);
315
+ if (validKeys.length >= allKeys.length || (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod)) {
316
+ setValue(displayValue);
317
+ }
318
+ } else {
319
+ setValue(addSegment(displayValue, type, amount, resolvedOptions));
320
+ }
270
321
  };
271
322
 
272
- let validationState: ValidationState = props.validationState ||
273
- (isInvalid(calendarValue, props.minValue, props.maxValue) ? 'invalid' : null);
323
+ let builtinValidation = useMemo(() => getValidationResult(
324
+ value,
325
+ minValue,
326
+ maxValue,
327
+ isDateUnavailable,
328
+ formatOpts
329
+ ), [value, minValue, maxValue, isDateUnavailable, formatOpts]);
330
+
331
+ let validation = useFormValidationState({
332
+ ...props,
333
+ value,
334
+ builtinValidation
335
+ });
336
+
337
+ let isValueInvalid = validation.displayValidation.isInvalid;
338
+ let validationState: ValidationState = props.validationState || (isValueInvalid ? 'invalid' : null);
274
339
 
275
340
  return {
341
+ ...validation,
276
342
  value: calendarValue,
277
343
  dateValue,
344
+ calendar,
278
345
  setValue,
279
346
  segments,
280
347
  dateFormatter,
281
348
  validationState,
349
+ isInvalid: isValueInvalid,
282
350
  granularity,
351
+ maxGranularity: props.maxGranularity ?? 'year',
283
352
  isDisabled,
284
353
  isReadOnly,
285
354
  isRequired,
@@ -299,26 +368,23 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
299
368
  markValid(part);
300
369
  setValue(setSegment(displayValue, part, v, resolvedOptions));
301
370
  },
302
- confirmPlaceholder(part) {
371
+ confirmPlaceholder() {
303
372
  if (props.isDisabled || props.isReadOnly) {
304
373
  return;
305
374
  }
306
375
 
307
- if (!part) {
308
- // Confirm the rest of the placeholder if any of the segments are valid.
309
- let numValid = Object.keys(validSegments).length;
310
- if (numValid > 0 && numValid < Object.keys(allSegments).length) {
311
- validSegments = {...allSegments};
312
- setValidSegments(validSegments);
313
- setValue(displayValue.copy());
314
- }
315
- } else if (!validSegments[part]) {
316
- markValid(part);
376
+ // Confirm the placeholder if only the day period is not filled in.
377
+ let validKeys = Object.keys(validSegments);
378
+ let allKeys = Object.keys(allSegments);
379
+ if (validKeys.length === allKeys.length - 1 && allSegments.dayPeriod && !validSegments.dayPeriod) {
380
+ validSegments = {...allSegments};
381
+ setValidSegments(validSegments);
317
382
  setValue(displayValue.copy());
318
383
  }
319
384
  },
320
385
  clearSegment(part) {
321
386
  delete validSegments[part];
387
+ clearedSegment.current = part;
322
388
  setValidSegments({...validSegments});
323
389
 
324
390
  let placeholder = createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone);
@@ -348,6 +414,11 @@ export function useDateFieldState(props: DatePickerFieldProps): DateFieldState {
348
414
  let formatOptions = getFormatOptions(fieldOptions, formatOpts);
349
415
  let formatter = new DateFormatter(locale, formatOptions);
350
416
  return formatter.format(dateValue);
417
+ },
418
+ getDateFormatter(locale, formatOptions: FormatterOptions) {
419
+ let newOptions = {...formatOpts, ...formatOptions};
420
+ let newFormatOptions = getFormatOptions({}, newOptions);
421
+ return new DateFormatter(locale, newFormatOptions);
351
422
  }
352
423
  };
353
424
  }
@@ -455,6 +526,7 @@ function setSegment(value: DateValue, part: string, segmentValue: number, option
455
526
  case 'day':
456
527
  case 'month':
457
528
  case 'year':
529
+ case 'era':
458
530
  return value.set({[part]: segmentValue});
459
531
  }
460
532
 
@@ -12,13 +12,14 @@
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, useDefaultProps} from './utils';
16
- import {isInvalid} from './utils';
15
+ import {FieldOptions, FormatterOptions, getFormatOptions, getPlaceholderTime, getValidationResult, useDefaultProps} from './utils';
16
+ import {FormValidationState, useFormValidationState} from '@react-stately/form';
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
- export interface DatePickerOptions extends DatePickerProps<DateValue> {
22
+ export interface DatePickerStateOptions<T extends DateValue> extends DatePickerProps<T> {
22
23
  /**
23
24
  * Determines whether the date picker popover should close automatically when a date is selected.
24
25
  * @default true
@@ -26,11 +27,11 @@ export interface DatePickerOptions extends DatePickerProps<DateValue> {
26
27
  shouldCloseOnSelect?: boolean | (() => boolean)
27
28
  }
28
29
 
29
- export interface DatePickerState {
30
+ export interface DatePickerState extends OverlayTriggerState, FormValidationState {
30
31
  /** The currently selected date. */
31
- value: DateValue,
32
+ value: DateValue | null,
32
33
  /** Sets the selected date. */
33
- setValue(value: DateValue): void,
34
+ setValue(value: DateValue | null): void,
34
35
  /**
35
36
  * The date portion of the value. This may be set prior to `value` if the user has
36
37
  * selected a date but has not yet selected a time.
@@ -53,24 +54,31 @@ export interface DatePickerState {
53
54
  isOpen: boolean,
54
55
  /** Sets whether the calendar popover is open. */
55
56
  setOpen(isOpen: boolean): void,
56
- /** The current validation state of the date picker, based on the `validationState`, `minValue`, and `maxValue` props. */
57
+ /**
58
+ * The current validation state of the date picker, based on the `validationState`, `minValue`, and `maxValue` props.
59
+ * @deprecated Use `isInvalid` instead.
60
+ */
57
61
  validationState: ValidationState,
62
+ /** Whether the date picker is invalid, based on the `isInvalid`, `minValue`, and `maxValue` props. */
63
+ isInvalid: boolean,
58
64
  /** Formats the selected value using the given options. */
59
- formatValue(locale: string, fieldOptions: FieldOptions): string
65
+ formatValue(locale: string, fieldOptions: FieldOptions): string,
66
+ /** Gets a formatter based on state's props. */
67
+ getDateFormatter(locale: string, formatOptions: FormatterOptions): DateFormatter
60
68
  }
61
69
 
62
70
  /**
63
71
  * Provides state management for a date picker component.
64
72
  * A date picker combines a DateField and a Calendar popover to allow users to enter or select a date and time value.
65
73
  */
66
- export function useDatePickerState(props: DatePickerOptions): DatePickerState {
67
- let [isOpen, setOpen] = useState(false);
74
+ export function useDatePickerState<T extends DateValue = DateValue>(props: DatePickerStateOptions<T>): DatePickerState {
75
+ let overlayState = useOverlayTriggerState(props);
68
76
  let [value, setValue] = useControlledState<DateValue>(props.value, props.defaultValue || null, props.onChange);
69
77
 
70
78
  let v = (value || props.placeholderValue);
71
79
  let [granularity, defaultTimeZone] = useDefaultProps(v, props.granularity);
72
80
  let dateValue = value != null ? value.toDate(defaultTimeZone ?? 'UTC') : null;
73
- let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second' || granularity === 'millisecond';
81
+ let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second';
74
82
  let shouldCloseOnSelect = props.shouldCloseOnSelect ?? true;
75
83
 
76
84
  let [selectedDate, setSelectedDate] = useState<DateValue>(null);
@@ -88,8 +96,39 @@ export function useDatePickerState(props: DatePickerOptions): DatePickerState {
88
96
  throw new Error('Invalid granularity ' + granularity + ' for value ' + v.toString());
89
97
  }
90
98
 
99
+ let showEra = value?.calendar.identifier === 'gregory' && value.era === 'BC';
100
+ let formatOpts = useMemo(() => ({
101
+ granularity,
102
+ timeZone: defaultTimeZone,
103
+ hideTimeZone: props.hideTimeZone,
104
+ hourCycle: props.hourCycle,
105
+ shouldForceLeadingZeros: props.shouldForceLeadingZeros,
106
+ showEra
107
+ }), [granularity, props.hourCycle, props.shouldForceLeadingZeros, defaultTimeZone, props.hideTimeZone, showEra]);
108
+
109
+ let {minValue, maxValue, isDateUnavailable} = props;
110
+ let builtinValidation = useMemo(() => getValidationResult(
111
+ value,
112
+ minValue,
113
+ maxValue,
114
+ isDateUnavailable,
115
+ formatOpts
116
+ ), [value, minValue, maxValue, isDateUnavailable, formatOpts]);
117
+
118
+ let validation = useFormValidationState({
119
+ ...props,
120
+ value,
121
+ builtinValidation
122
+ });
123
+
124
+ let isValueInvalid = validation.displayValidation.isInvalid;
125
+ let validationState: ValidationState = props.validationState || (isValueInvalid ? 'invalid' : null);
126
+
91
127
  let commitValue = (date: DateValue, time: TimeValue) => {
92
128
  setValue('timeZone' in time ? time.set(toCalendarDate(date)) : toCalendarDateTime(date, time));
129
+ setSelectedDate(null);
130
+ setSelectedTime(null);
131
+ validation.commitValidation();
93
132
  };
94
133
 
95
134
  // Intercept setValue to make sure the Time section is not changed by date selection in Calendar
@@ -103,26 +142,24 @@ export function useDatePickerState(props: DatePickerOptions): DatePickerState {
103
142
  }
104
143
  } else {
105
144
  setValue(newValue);
145
+ validation.commitValidation();
106
146
  }
107
147
 
108
148
  if (shouldClose) {
109
- setOpen(false);
149
+ overlayState.setOpen(false);
110
150
  }
111
151
  };
112
152
 
113
153
  let selectTime = (newValue: TimeValue) => {
114
- if (selectedDate) {
154
+ if (selectedDate && newValue) {
115
155
  commitValue(selectedDate, newValue);
116
156
  } else {
117
157
  setSelectedTime(newValue);
118
158
  }
119
159
  };
120
160
 
121
- let validationState: ValidationState = props.validationState ||
122
- (isInvalid(value, props.minValue, props.maxValue) ? 'invalid' : null) ||
123
- (value && props.isDateUnavailable?.(value) ? 'invalid' : null);
124
-
125
161
  return {
162
+ ...validation,
126
163
  value,
127
164
  setValue,
128
165
  dateValue: selectedDate,
@@ -131,7 +168,7 @@ export function useDatePickerState(props: DatePickerOptions): DatePickerState {
131
168
  setTimeValue: selectTime,
132
169
  granularity,
133
170
  hasTime,
134
- isOpen,
171
+ ...overlayState,
135
172
  setOpen(isOpen) {
136
173
  // Commit the selected date when the calendar is closed. Use a placeholder time if one wasn't set.
137
174
  // If only the time was set and not the date, don't commit. The state will be preserved until
@@ -140,23 +177,23 @@ export function useDatePickerState(props: DatePickerOptions): DatePickerState {
140
177
  commitValue(selectedDate, selectedTime || getPlaceholderTime(props.placeholderValue));
141
178
  }
142
179
 
143
- setOpen(isOpen);
180
+ overlayState.setOpen(isOpen);
144
181
  },
145
182
  validationState,
183
+ isInvalid: isValueInvalid,
146
184
  formatValue(locale, fieldOptions) {
147
185
  if (!dateValue) {
148
186
  return '';
149
187
  }
150
188
 
151
- let formatOptions = getFormatOptions(fieldOptions, {
152
- granularity,
153
- timeZone: defaultTimeZone,
154
- hideTimeZone: props.hideTimeZone,
155
- hourCycle: props.hourCycle
156
- });
157
-
189
+ let formatOptions = getFormatOptions(fieldOptions, formatOpts);
158
190
  let formatter = new DateFormatter(locale, formatOptions);
159
191
  return formatter.format(dateValue);
192
+ },
193
+ getDateFormatter(locale, formatOptions: FormatterOptions) {
194
+ let newOptions = {...formatOpts, ...formatOptions};
195
+ let newFormatOptions = getFormatOptions({}, newOptions);
196
+ return new DateFormatter(locale, newFormatOptions);
160
197
  }
161
198
  };
162
199
  }