@react-stately/datepicker 3.0.0-nightly.3113 → 3.0.0-nightly.3114

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 (220) 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 -744
  103. package/dist/main.js.map +1 -1
  104. package/dist/module.js +16 -730
  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 +208 -41
  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 +16 -9
  214. package/src/index.ts +9 -4
  215. package/src/placeholders.ts +108 -0
  216. package/src/{useDatePickerFieldState.ts → useDateFieldState.ts} +199 -59
  217. package/src/useDatePickerState.ts +104 -32
  218. package/src/useDateRangePickerState.ts +155 -55
  219. package/src/useTimeFieldState.ts +37 -12
  220. package/src/utils.ts +140 -18
@@ -10,39 +10,92 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {Calendar, CalendarDateTime, DateFormatter, getMinimumDayInMonth, getMinimumMonthInYear, GregorianCalendar, toCalendar} from '@internationalized/date';
14
- import {convertValue, createPlaceholderDate, FieldOptions, getFormatOptions, isInvalid, useDefaultProps} from './utils';
13
+ import {Calendar, DateFormatter, getMinimumDayInMonth, getMinimumMonthInYear, GregorianCalendar, toCalendar} from '@internationalized/date';
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';
19
21
 
22
+ export type SegmentType = 'era' | 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'dayPeriod' | 'literal' | 'timeZoneName';
20
23
  export interface DateSegment {
21
- type: Intl.DateTimeFormatPartTypes,
24
+ /** The type of segment. */
25
+ type: SegmentType,
26
+ /** The formatted text for the segment. */
22
27
  text: string,
28
+ /** The numeric value for the segment, if applicable. */
23
29
  value?: number,
30
+ /** The minimum numeric value for the segment, if applicable. */
24
31
  minValue?: number,
32
+ /** The maximum numeric value for the segment, if applicable. */
25
33
  maxValue?: number,
34
+ /** Whether the value is a placeholder. */
26
35
  isPlaceholder: boolean,
36
+ /** A placeholder string for the segment. */
37
+ placeholder: string,
38
+ /** Whether the segment is editable. */
27
39
  isEditable: boolean
28
40
  }
29
41
 
30
- export interface DatePickerFieldState {
42
+ export interface DateFieldState extends FormValidationState {
43
+ /** The current field value. */
31
44
  value: DateValue,
45
+ /** The current value, converted to a native JavaScript `Date` object. */
32
46
  dateValue: Date,
33
- setValue: (value: CalendarDateTime) => void,
47
+ /** The calendar system currently in use. */
48
+ calendar: Calendar,
49
+ /** Sets the field's value. */
50
+ setValue(value: DateValue): void,
51
+ /** A list of segments for the current value. */
34
52
  segments: DateSegment[],
53
+ /** A date formatter configured for the current locale and format. */
35
54
  dateFormatter: DateFormatter,
55
+ /**
56
+ * The current validation state of the date field, based on the `validationState`, `minValue`, and `maxValue` props.
57
+ * @deprecated Use `isInvalid` instead.
58
+ */
36
59
  validationState: ValidationState,
60
+ /** Whether the date field is invalid, based on the `isInvalid`, `minValue`, and `maxValue` props. */
61
+ isInvalid: boolean,
62
+ /** The granularity for the field, based on the `granularity` prop and current value. */
37
63
  granularity: Granularity,
38
- increment: (type: Intl.DateTimeFormatPartTypes) => void,
39
- decrement: (type: Intl.DateTimeFormatPartTypes) => void,
40
- incrementPage: (type: Intl.DateTimeFormatPartTypes) => void,
41
- decrementPage: (type: Intl.DateTimeFormatPartTypes) => void,
42
- setSegment: (type: Intl.DateTimeFormatPartTypes, value: number) => void,
43
- confirmPlaceholder: (type?: Intl.DateTimeFormatPartTypes) => void,
44
- clearSegment: (type?: Intl.DateTimeFormatPartTypes) => void,
45
- getFormatOptions(fieldOptions: FieldOptions): Intl.DateTimeFormatOptions
64
+ /** The maximum date or time unit that is displayed in the field. */
65
+ maxGranularity: 'year' | 'month' | Granularity,
66
+ /** Whether the field is disabled. */
67
+ isDisabled: boolean,
68
+ /** Whether the field is read only. */
69
+ isReadOnly: boolean,
70
+ /** Whether the field is required. */
71
+ isRequired: boolean,
72
+ /** Increments the given segment. Upon reaching the minimum or maximum value, the value wraps around to the opposite limit. */
73
+ increment(type: SegmentType): void,
74
+ /** Decrements the given segment. Upon reaching the minimum or maximum value, the value wraps around to the opposite limit. */
75
+ decrement(type: SegmentType): void,
76
+ /**
77
+ * Increments the given segment by a larger amount, rounding it to the nearest increment.
78
+ * The amount to increment by depends on the field, for example 15 minutes, 7 days, and 5 years.
79
+ * Upon reaching the minimum or maximum value, the value wraps around to the opposite limit.
80
+ */
81
+ incrementPage(type: SegmentType): void,
82
+ /**
83
+ * Decrements the given segment by a larger amount, rounding it to the nearest increment.
84
+ * The amount to decrement by depends on the field, for example 15 minutes, 7 days, and 5 years.
85
+ * Upon reaching the minimum or maximum value, the value wraps around to the opposite limit.
86
+ */
87
+ decrementPage(type: SegmentType): void,
88
+ /** Sets the value of the given segment. */
89
+ setSegment(type: 'era', value: string): void,
90
+ setSegment(type: SegmentType, value: number): void,
91
+ /** Updates the remaining unfilled segments with the placeholder value. */
92
+ confirmPlaceholder(): void,
93
+ /** Clears the value of the given segment, reverting it to the placeholder. */
94
+ clearSegment(type: SegmentType): void,
95
+ /** Formats the current date value using the given options. */
96
+ formatValue(fieldOptions: FieldOptions): string,
97
+ /** Gets a formatter based on state's props. */
98
+ getDateFormatter(locale: string, formatOptions: FormatterOptions): DateFormatter
46
99
  }
47
100
 
48
101
  const EDITABLE_SEGMENTS = {
@@ -70,17 +123,39 @@ const TYPE_MAPPING = {
70
123
  dayperiod: 'dayPeriod'
71
124
  };
72
125
 
73
- interface DatePickerFieldProps<T extends DateValue> extends DatePickerProps<T> {
74
- maxGranularity?: 'year' | 'month' | DatePickerProps<T>['granularity'],
126
+ export interface DateFieldStateOptions<T extends DateValue = DateValue> extends DatePickerProps<T> {
127
+ /**
128
+ * The maximum unit to display in the date field.
129
+ * @default 'year'
130
+ */
131
+ maxGranularity?: 'year' | 'month' | Granularity,
132
+ /** The locale to display and edit the value according to. */
75
133
  locale: string,
134
+ /**
135
+ * A function that creates a [Calendar](../internationalized/date/Calendar.html)
136
+ * object for a given calendar identifier. Such a function may be imported from the
137
+ * `@internationalized/date` package, or manually implemented to include support for
138
+ * only certain calendars.
139
+ */
76
140
  createCalendar: (name: string) => Calendar
77
141
  }
78
142
 
79
- export function useDatePickerFieldState<T extends DateValue>(props: DatePickerFieldProps<T>): DatePickerFieldState {
143
+ /**
144
+ * Provides state management for a date field component.
145
+ * A date field allows users to enter and edit date and time values using a keyboard.
146
+ * Each part of a date value is displayed in an individually editable segment.
147
+ */
148
+ export function useDateFieldState<T extends DateValue = DateValue>(props: DateFieldStateOptions<T>): DateFieldState {
80
149
  let {
81
150
  locale,
82
151
  createCalendar,
83
- hideTimeZone
152
+ hideTimeZone,
153
+ isDisabled,
154
+ isReadOnly,
155
+ isRequired,
156
+ minValue,
157
+ maxValue,
158
+ isDateUnavailable
84
159
  } = props;
85
160
 
86
161
  let v: DateValue = (props.value || props.defaultValue || props.placeholderValue);
@@ -92,22 +167,44 @@ export function useDatePickerFieldState<T extends DateValue>(props: DatePickerFi
92
167
  throw new Error('Invalid granularity ' + granularity + ' for value ' + v.toString());
93
168
  }
94
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';
95
191
  let formatOpts = useMemo(() => ({
96
192
  granularity,
97
193
  maxGranularity: props.maxGranularity ?? 'year',
98
194
  timeZone: defaultTimeZone,
99
195
  hideTimeZone,
100
- hourCycle: props.hourCycle
101
- }), [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]);
102
200
  let opts = useMemo(() => getFormatOptions({}, formatOpts), [formatOpts]);
103
201
 
104
202
  let dateFormatter = useMemo(() => new DateFormatter(locale, opts), [locale, opts]);
105
203
  let resolvedOptions = useMemo(() => dateFormatter.resolvedOptions(), [dateFormatter]);
106
- let calendar = useMemo(() => createCalendar(resolvedOptions.calendar), [createCalendar, resolvedOptions.calendar]);
107
204
 
108
205
  // Determine how many editable segments there are for validation purposes.
109
206
  // The result is cached for performance.
110
- let allSegments = useMemo(() =>
207
+ let allSegments: Partial<typeof EDITABLE_SEGMENTS> = useMemo(() =>
111
208
  dateFormatter.formatToParts(new Date())
112
209
  .filter(seg => EDITABLE_SEGMENTS[seg.type])
113
210
  .reduce((p, seg) => (p[seg.type] = true, p), {})
@@ -117,13 +214,7 @@ export function useDatePickerFieldState<T extends DateValue>(props: DatePickerFi
117
214
  () => props.value || props.defaultValue ? {...allSegments} : {}
118
215
  );
119
216
 
120
- // We keep track of the placeholder date separately in state so that onChange is not called
121
- // until all segments are set. If the value === null (not undefined), then assume the component
122
- // is controlled, so use the placeholder as the value until all segments are entered so it doesn't
123
- // change from uncontrolled to controlled and emit a warning.
124
- let [placeholderDate, setPlaceholderDate] = useState(
125
- () => createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone)
126
- );
217
+ let clearedSegment = useRef<string>(undefined);
127
218
 
128
219
  // Reset placeholder when calendar changes
129
220
  let lastCalendarIdentifier = useRef(calendar.identifier);
@@ -138,14 +229,6 @@ export function useDatePickerFieldState<T extends DateValue>(props: DatePickerFi
138
229
  }
139
230
  }, [calendar, granularity, validSegments, defaultTimeZone, props.placeholderValue]);
140
231
 
141
- let [value, setDate] = useControlledState<DateValue>(
142
- props.value,
143
- props.defaultValue,
144
- props.onChange
145
- );
146
-
147
- let calendarValue = useMemo(() => convertValue(value, calendar), [value, calendar]);
148
-
149
232
  // If there is a value prop, and some segments were previously placeholders, mark them all as valid.
150
233
  if (value && Object.keys(validSegments).length < Object.keys(allSegments).length) {
151
234
  validSegments = {...allSegments};
@@ -165,8 +248,15 @@ export function useDatePickerFieldState<T extends DateValue>(props: DatePickerFi
165
248
  if (props.isDisabled || props.isReadOnly) {
166
249
  return;
167
250
  }
251
+ let validKeys = Object.keys(validSegments);
252
+ let allKeys = Object.keys(allSegments);
168
253
 
169
- 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')) {
170
260
  // The display calendar should not have any effect on the emitted value.
171
261
  // Emit dates in the same calendar as the original value, if any, otherwise gregorian.
172
262
  newValue = toCalendar(newValue, v?.calendar || new GregorianCalendar());
@@ -174,6 +264,7 @@ export function useDatePickerFieldState<T extends DateValue>(props: DatePickerFi
174
264
  } else {
175
265
  setPlaceholderDate(newValue);
176
266
  }
267
+ clearedSegment.current = null;
177
268
  };
178
269
 
179
270
  let dateValue = useMemo(() => displayValue.toDate(timeZone), [displayValue, timeZone]);
@@ -185,42 +276,82 @@ export function useDatePickerFieldState<T extends DateValue>(props: DatePickerFi
185
276
  isEditable = false;
186
277
  }
187
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;
188
281
  return {
189
282
  type: TYPE_MAPPING[segment.type] || segment.type,
190
- text: segment.value,
283
+ text: isPlaceholder ? placeholder : segment.value,
191
284
  ...getSegmentLimits(displayValue, segment.type, resolvedOptions),
192
- isPlaceholder: EDITABLE_SEGMENTS[segment.type] && !validSegments[segment.type],
285
+ isPlaceholder,
286
+ placeholder,
193
287
  isEditable
194
288
  } as DateSegment;
195
289
  })
196
- , [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar]);
290
+ , [dateValue, validSegments, dateFormatter, resolvedOptions, displayValue, calendar, locale]);
197
291
 
198
- 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
+ }
199
301
 
200
302
  let markValid = (part: Intl.DateTimeFormatPartTypes) => {
201
303
  validSegments[part] = true;
202
- if (part === 'year' && hasEra) {
304
+ if (part === 'year' && allSegments.era) {
203
305
  validSegments.era = true;
204
306
  }
205
307
  setValidSegments({...validSegments});
206
308
  };
207
309
 
208
310
  let adjustSegment = (type: Intl.DateTimeFormatPartTypes, amount: number) => {
209
- markValid(type);
210
- 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
+ }
211
321
  };
212
322
 
213
- let validationState: ValidationState = props.validationState ||
214
- (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);
215
339
 
216
340
  return {
341
+ ...validation,
217
342
  value: calendarValue,
218
343
  dateValue,
344
+ calendar,
219
345
  setValue,
220
346
  segments,
221
347
  dateFormatter,
222
348
  validationState,
349
+ isInvalid: isValueInvalid,
223
350
  granularity,
351
+ maxGranularity: props.maxGranularity ?? 'year',
352
+ isDisabled,
353
+ isReadOnly,
354
+ isRequired,
224
355
  increment(part) {
225
356
  adjustSegment(part, 1);
226
357
  },
@@ -237,26 +368,23 @@ export function useDatePickerFieldState<T extends DateValue>(props: DatePickerFi
237
368
  markValid(part);
238
369
  setValue(setSegment(displayValue, part, v, resolvedOptions));
239
370
  },
240
- confirmPlaceholder(part) {
371
+ confirmPlaceholder() {
241
372
  if (props.isDisabled || props.isReadOnly) {
242
373
  return;
243
374
  }
244
375
 
245
- if (!part) {
246
- // Confirm the rest of the placeholder if any of the segments are valid.
247
- let numValid = Object.keys(validSegments).length;
248
- if (numValid > 0 && numValid < Object.keys(allSegments).length) {
249
- validSegments = {...allSegments};
250
- setValidSegments(validSegments);
251
- setValue(displayValue.copy());
252
- }
253
- } else if (!validSegments[part]) {
254
- 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);
255
382
  setValue(displayValue.copy());
256
383
  }
257
384
  },
258
385
  clearSegment(part) {
259
386
  delete validSegments[part];
387
+ clearedSegment.current = part;
260
388
  setValidSegments({...validSegments});
261
389
 
262
390
  let placeholder = createPlaceholderDate(props.placeholderValue, granularity, calendar, defaultTimeZone);
@@ -278,8 +406,19 @@ export function useDatePickerFieldState<T extends DateValue>(props: DatePickerFi
278
406
  setDate(null);
279
407
  setValue(value);
280
408
  },
281
- getFormatOptions(fieldOptions: FieldOptions) {
282
- return getFormatOptions(fieldOptions, formatOpts);
409
+ formatValue(fieldOptions: FieldOptions) {
410
+ if (!calendarValue) {
411
+ return '';
412
+ }
413
+
414
+ let formatOptions = getFormatOptions(fieldOptions, formatOpts);
415
+ let formatter = new DateFormatter(locale, formatOptions);
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);
283
422
  }
284
423
  };
285
424
  }
@@ -387,6 +526,7 @@ function setSegment(value: DateValue, part: string, segmentValue: number, option
387
526
  case 'day':
388
527
  case 'month':
389
528
  case 'year':
529
+ case 'era':
390
530
  return value.set({[part]: segmentValue});
391
531
  }
392
532
 
@@ -10,36 +10,76 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {CalendarDate, DateFormatter, toCalendarDateTime, toDateFields} from '@internationalized/date';
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 DatePickerState {
22
- value: DateValue,
23
- setValue: (value: DateValue) => void,
22
+ export interface DatePickerStateOptions<T extends DateValue> extends DatePickerProps<T> {
23
+ /**
24
+ * Determines whether the date picker popover should close automatically when a date is selected.
25
+ * @default true
26
+ */
27
+ shouldCloseOnSelect?: boolean | (() => boolean)
28
+ }
29
+
30
+ export interface DatePickerState extends OverlayTriggerState, FormValidationState {
31
+ /** The currently selected date. */
32
+ value: DateValue | null,
33
+ /** Sets the selected date. */
34
+ setValue(value: DateValue | null): void,
35
+ /**
36
+ * The date portion of the value. This may be set prior to `value` if the user has
37
+ * selected a date but has not yet selected a time.
38
+ */
24
39
  dateValue: DateValue,
25
- setDateValue: (value: CalendarDate) => void,
40
+ /** Sets the date portion of the value. */
41
+ setDateValue(value: CalendarDate): void,
42
+ /**
43
+ * The time portion of the value. This may be set prior to `value` if the user has
44
+ * selected a time but has not yet selected a date.
45
+ */
26
46
  timeValue: TimeValue,
27
- setTimeValue: (value: TimeValue) => void,
47
+ /** Sets the time portion of the value. */
48
+ setTimeValue(value: TimeValue): void,
49
+ /** The granularity for the field, based on the `granularity` prop and current value. */
50
+ granularity: Granularity,
51
+ /** Whether the date picker supports selecting a time, according to the `granularity` prop and current value. */
52
+ hasTime: boolean,
53
+ /** Whether the calendar popover is currently open. */
28
54
  isOpen: boolean,
29
- setOpen: (isOpen: boolean) => void,
55
+ /** Sets whether the calendar popover is open. */
56
+ setOpen(isOpen: boolean): void,
57
+ /**
58
+ * The current validation state of the date picker, based on the `validationState`, `minValue`, and `maxValue` props.
59
+ * @deprecated Use `isInvalid` instead.
60
+ */
30
61
  validationState: ValidationState,
62
+ /** Whether the date picker is invalid, based on the `isInvalid`, `minValue`, and `maxValue` props. */
63
+ isInvalid: boolean,
64
+ /** Formats the selected value using the given options. */
31
65
  formatValue(locale: string, fieldOptions: FieldOptions): string,
32
- granularity: Granularity
66
+ /** Gets a formatter based on state's props. */
67
+ getDateFormatter(locale: string, formatOptions: FormatterOptions): DateFormatter
33
68
  }
34
69
 
35
- export function useDatePickerState<T extends DateValue>(props: DatePickerProps<T>): DatePickerState {
36
- let [isOpen, setOpen] = useState(false);
70
+ /**
71
+ * Provides state management for a date picker component.
72
+ * A date picker combines a DateField and a Calendar popover to allow users to enter or select a date and time value.
73
+ */
74
+ export function useDatePickerState<T extends DateValue = DateValue>(props: DatePickerStateOptions<T>): DatePickerState {
75
+ let overlayState = useOverlayTriggerState(props);
37
76
  let [value, setValue] = useControlledState<DateValue>(props.value, props.defaultValue || null, props.onChange);
38
77
 
39
78
  let v = (value || props.placeholderValue);
40
79
  let [granularity, defaultTimeZone] = useDefaultProps(v, props.granularity);
41
80
  let dateValue = value != null ? value.toDate(defaultTimeZone ?? 'UTC') : null;
42
- let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second' || granularity === 'millisecond';
81
+ let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second';
82
+ let shouldCloseOnSelect = props.shouldCloseOnSelect ?? true;
43
83
 
44
84
  let [selectedDate, setSelectedDate] = useState<DateValue>(null);
45
85
  let [selectedTime, setSelectedTime] = useState<TimeValue>(null);
@@ -56,39 +96,70 @@ export function useDatePickerState<T extends DateValue>(props: DatePickerProps<T
56
96
  throw new Error('Invalid granularity ' + granularity + ' for value ' + v.toString());
57
97
  }
58
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
+
59
127
  let commitValue = (date: DateValue, time: TimeValue) => {
60
- setValue('timeZone' in time ? time.set(toDateFields(date)) : toCalendarDateTime(date, time));
128
+ setValue('timeZone' in time ? time.set(toCalendarDate(date)) : toCalendarDateTime(date, time));
129
+ setSelectedDate(null);
130
+ setSelectedTime(null);
131
+ validation.commitValidation();
61
132
  };
62
133
 
63
134
  // Intercept setValue to make sure the Time section is not changed by date selection in Calendar
64
135
  let selectDate = (newValue: CalendarDate) => {
136
+ let shouldClose = typeof shouldCloseOnSelect === 'function' ? shouldCloseOnSelect() : shouldCloseOnSelect;
65
137
  if (hasTime) {
66
- if (selectedTime) {
67
- commitValue(newValue, selectedTime);
138
+ if (selectedTime || shouldClose) {
139
+ commitValue(newValue, selectedTime || getPlaceholderTime(props.placeholderValue));
68
140
  } else {
69
141
  setSelectedDate(newValue);
70
142
  }
71
143
  } else {
72
144
  setValue(newValue);
145
+ validation.commitValidation();
73
146
  }
74
147
 
75
- if (!hasTime) {
76
- setOpen(false);
148
+ if (shouldClose) {
149
+ overlayState.setOpen(false);
77
150
  }
78
151
  };
79
152
 
80
153
  let selectTime = (newValue: TimeValue) => {
81
- if (selectedDate) {
154
+ if (selectedDate && newValue) {
82
155
  commitValue(selectedDate, newValue);
83
156
  } else {
84
157
  setSelectedTime(newValue);
85
158
  }
86
159
  };
87
160
 
88
- let validationState: ValidationState = props.validationState ||
89
- (isInvalid(value, props.minValue, props.maxValue) ? 'invalid' : null);
90
-
91
161
  return {
162
+ ...validation,
92
163
  value,
93
164
  setValue,
94
165
  dateValue: selectedDate,
@@ -96,7 +167,8 @@ export function useDatePickerState<T extends DateValue>(props: DatePickerProps<T
96
167
  setDateValue: selectDate,
97
168
  setTimeValue: selectTime,
98
169
  granularity,
99
- isOpen,
170
+ hasTime,
171
+ ...overlayState,
100
172
  setOpen(isOpen) {
101
173
  // Commit the selected date when the calendar is closed. Use a placeholder time if one wasn't set.
102
174
  // If only the time was set and not the date, don't commit. The state will be preserved until
@@ -105,23 +177,23 @@ export function useDatePickerState<T extends DateValue>(props: DatePickerProps<T
105
177
  commitValue(selectedDate, selectedTime || getPlaceholderTime(props.placeholderValue));
106
178
  }
107
179
 
108
- setOpen(isOpen);
180
+ overlayState.setOpen(isOpen);
109
181
  },
110
182
  validationState,
183
+ isInvalid: isValueInvalid,
111
184
  formatValue(locale, fieldOptions) {
112
185
  if (!dateValue) {
113
186
  return '';
114
187
  }
115
188
 
116
- let formatOptions = getFormatOptions(fieldOptions, {
117
- granularity,
118
- timeZone: defaultTimeZone,
119
- hideTimeZone: props.hideTimeZone,
120
- hourCycle: props.hourCycle
121
- });
122
-
189
+ let formatOptions = getFormatOptions(fieldOptions, formatOpts);
123
190
  let formatter = new DateFormatter(locale, formatOptions);
124
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);
125
197
  }
126
198
  };
127
199
  }