@react-aria/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 +21 -0
  2. package/dist/ar-AE.main.js.map +1 -0
  3. package/dist/ar-AE.mjs +23 -0
  4. package/dist/ar-AE.module.js +23 -0
  5. package/dist/ar-AE.module.js.map +1 -0
  6. package/dist/bg-BG.main.js +21 -0
  7. package/dist/bg-BG.main.js.map +1 -0
  8. package/dist/bg-BG.mjs +23 -0
  9. package/dist/bg-BG.module.js +23 -0
  10. package/dist/bg-BG.module.js.map +1 -0
  11. package/dist/cs-CZ.main.js +21 -0
  12. package/dist/cs-CZ.main.js.map +1 -0
  13. package/dist/cs-CZ.mjs +23 -0
  14. package/dist/cs-CZ.module.js +23 -0
  15. package/dist/cs-CZ.module.js.map +1 -0
  16. package/dist/da-DK.main.js +21 -0
  17. package/dist/da-DK.main.js.map +1 -0
  18. package/dist/da-DK.mjs +23 -0
  19. package/dist/da-DK.module.js +23 -0
  20. package/dist/da-DK.module.js.map +1 -0
  21. package/dist/de-DE.main.js +21 -0
  22. package/dist/de-DE.main.js.map +1 -0
  23. package/dist/de-DE.mjs +23 -0
  24. package/dist/de-DE.module.js +23 -0
  25. package/dist/de-DE.module.js.map +1 -0
  26. package/dist/el-GR.main.js +21 -0
  27. package/dist/el-GR.main.js.map +1 -0
  28. package/dist/el-GR.mjs +23 -0
  29. package/dist/el-GR.module.js +23 -0
  30. package/dist/el-GR.module.js.map +1 -0
  31. package/dist/en-US.main.js +21 -0
  32. package/dist/en-US.main.js.map +1 -0
  33. package/dist/en-US.mjs +23 -0
  34. package/dist/en-US.module.js +23 -0
  35. package/dist/en-US.module.js.map +1 -0
  36. package/dist/es-ES.main.js +21 -0
  37. package/dist/es-ES.main.js.map +1 -0
  38. package/dist/es-ES.mjs +23 -0
  39. package/dist/es-ES.module.js +23 -0
  40. package/dist/es-ES.module.js.map +1 -0
  41. package/dist/et-EE.main.js +21 -0
  42. package/dist/et-EE.main.js.map +1 -0
  43. package/dist/et-EE.mjs +23 -0
  44. package/dist/et-EE.module.js +23 -0
  45. package/dist/et-EE.module.js.map +1 -0
  46. package/dist/fi-FI.main.js +21 -0
  47. package/dist/fi-FI.main.js.map +1 -0
  48. package/dist/fi-FI.mjs +23 -0
  49. package/dist/fi-FI.module.js +23 -0
  50. package/dist/fi-FI.module.js.map +1 -0
  51. package/dist/fr-FR.main.js +21 -0
  52. package/dist/fr-FR.main.js.map +1 -0
  53. package/dist/fr-FR.mjs +23 -0
  54. package/dist/fr-FR.module.js +23 -0
  55. package/dist/fr-FR.module.js.map +1 -0
  56. package/dist/he-IL.main.js +21 -0
  57. package/dist/he-IL.main.js.map +1 -0
  58. package/dist/he-IL.mjs +23 -0
  59. package/dist/he-IL.module.js +23 -0
  60. package/dist/he-IL.module.js.map +1 -0
  61. package/dist/hr-HR.main.js +21 -0
  62. package/dist/hr-HR.main.js.map +1 -0
  63. package/dist/hr-HR.mjs +23 -0
  64. package/dist/hr-HR.module.js +23 -0
  65. package/dist/hr-HR.module.js.map +1 -0
  66. package/dist/hu-HU.main.js +21 -0
  67. package/dist/hu-HU.main.js.map +1 -0
  68. package/dist/hu-HU.mjs +23 -0
  69. package/dist/hu-HU.module.js +23 -0
  70. package/dist/hu-HU.module.js.map +1 -0
  71. package/dist/import.mjs +25 -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 +21 -0
  78. package/dist/it-IT.main.js.map +1 -0
  79. package/dist/it-IT.mjs +23 -0
  80. package/dist/it-IT.module.js +23 -0
  81. package/dist/it-IT.module.js.map +1 -0
  82. package/dist/ja-JP.main.js +21 -0
  83. package/dist/ja-JP.main.js.map +1 -0
  84. package/dist/ja-JP.mjs +23 -0
  85. package/dist/ja-JP.module.js +23 -0
  86. package/dist/ja-JP.module.js.map +1 -0
  87. package/dist/ko-KR.main.js +21 -0
  88. package/dist/ko-KR.main.js.map +1 -0
  89. package/dist/ko-KR.mjs +23 -0
  90. package/dist/ko-KR.module.js +23 -0
  91. package/dist/ko-KR.module.js.map +1 -0
  92. package/dist/lt-LT.main.js +21 -0
  93. package/dist/lt-LT.main.js.map +1 -0
  94. package/dist/lt-LT.mjs +23 -0
  95. package/dist/lt-LT.module.js +23 -0
  96. package/dist/lt-LT.module.js.map +1 -0
  97. package/dist/lv-LV.main.js +21 -0
  98. package/dist/lv-LV.main.js.map +1 -0
  99. package/dist/lv-LV.mjs +23 -0
  100. package/dist/lv-LV.module.js +23 -0
  101. package/dist/lv-LV.module.js.map +1 -0
  102. package/dist/main.js +22 -759
  103. package/dist/main.js.map +1 -1
  104. package/dist/module.js +17 -744
  105. package/dist/module.js.map +1 -1
  106. package/dist/nb-NO.main.js +21 -0
  107. package/dist/nb-NO.main.js.map +1 -0
  108. package/dist/nb-NO.mjs +23 -0
  109. package/dist/nb-NO.module.js +23 -0
  110. package/dist/nb-NO.module.js.map +1 -0
  111. package/dist/nl-NL.main.js +21 -0
  112. package/dist/nl-NL.main.js.map +1 -0
  113. package/dist/nl-NL.mjs +23 -0
  114. package/dist/nl-NL.module.js +23 -0
  115. package/dist/nl-NL.module.js.map +1 -0
  116. package/dist/pl-PL.main.js +21 -0
  117. package/dist/pl-PL.main.js.map +1 -0
  118. package/dist/pl-PL.mjs +23 -0
  119. package/dist/pl-PL.module.js +23 -0
  120. package/dist/pl-PL.module.js.map +1 -0
  121. package/dist/pt-BR.main.js +21 -0
  122. package/dist/pt-BR.main.js.map +1 -0
  123. package/dist/pt-BR.mjs +23 -0
  124. package/dist/pt-BR.module.js +23 -0
  125. package/dist/pt-BR.module.js.map +1 -0
  126. package/dist/pt-PT.main.js +21 -0
  127. package/dist/pt-PT.main.js.map +1 -0
  128. package/dist/pt-PT.mjs +23 -0
  129. package/dist/pt-PT.module.js +23 -0
  130. package/dist/pt-PT.module.js.map +1 -0
  131. package/dist/ro-RO.main.js +21 -0
  132. package/dist/ro-RO.main.js.map +1 -0
  133. package/dist/ro-RO.mjs +23 -0
  134. package/dist/ro-RO.module.js +23 -0
  135. package/dist/ro-RO.module.js.map +1 -0
  136. package/dist/ru-RU.main.js +21 -0
  137. package/dist/ru-RU.main.js.map +1 -0
  138. package/dist/ru-RU.mjs +23 -0
  139. package/dist/ru-RU.module.js +23 -0
  140. package/dist/ru-RU.module.js.map +1 -0
  141. package/dist/sk-SK.main.js +21 -0
  142. package/dist/sk-SK.main.js.map +1 -0
  143. package/dist/sk-SK.mjs +23 -0
  144. package/dist/sk-SK.module.js +23 -0
  145. package/dist/sk-SK.module.js.map +1 -0
  146. package/dist/sl-SI.main.js +21 -0
  147. package/dist/sl-SI.main.js.map +1 -0
  148. package/dist/sl-SI.mjs +23 -0
  149. package/dist/sl-SI.module.js +23 -0
  150. package/dist/sl-SI.module.js.map +1 -0
  151. package/dist/sr-SP.main.js +21 -0
  152. package/dist/sr-SP.main.js.map +1 -0
  153. package/dist/sr-SP.mjs +23 -0
  154. package/dist/sr-SP.module.js +23 -0
  155. package/dist/sr-SP.module.js.map +1 -0
  156. package/dist/sv-SE.main.js +21 -0
  157. package/dist/sv-SE.main.js.map +1 -0
  158. package/dist/sv-SE.mjs +23 -0
  159. package/dist/sv-SE.module.js +23 -0
  160. package/dist/sv-SE.module.js.map +1 -0
  161. package/dist/tr-TR.main.js +21 -0
  162. package/dist/tr-TR.main.js.map +1 -0
  163. package/dist/tr-TR.mjs +23 -0
  164. package/dist/tr-TR.module.js +23 -0
  165. package/dist/tr-TR.module.js.map +1 -0
  166. package/dist/types.d.ts +88 -37
  167. package/dist/types.d.ts.map +1 -1
  168. package/dist/uk-UA.main.js +21 -0
  169. package/dist/uk-UA.main.js.map +1 -0
  170. package/dist/uk-UA.mjs +23 -0
  171. package/dist/uk-UA.module.js +23 -0
  172. package/dist/uk-UA.module.js.map +1 -0
  173. package/dist/useDateField.main.js +177 -0
  174. package/dist/useDateField.main.js.map +1 -0
  175. package/dist/useDateField.mjs +168 -0
  176. package/dist/useDateField.module.js +168 -0
  177. package/dist/useDateField.module.js.map +1 -0
  178. package/dist/useDatePicker.main.js +158 -0
  179. package/dist/useDatePicker.main.js.map +1 -0
  180. package/dist/useDatePicker.mjs +153 -0
  181. package/dist/useDatePicker.module.js +153 -0
  182. package/dist/useDatePicker.module.js.map +1 -0
  183. package/dist/useDatePickerGroup.main.js +91 -0
  184. package/dist/useDatePickerGroup.main.js.map +1 -0
  185. package/dist/useDatePickerGroup.mjs +86 -0
  186. package/dist/useDatePickerGroup.module.js +86 -0
  187. package/dist/useDatePickerGroup.module.js.map +1 -0
  188. package/dist/useDateRangePicker.main.js +201 -0
  189. package/dist/useDateRangePicker.main.js.map +1 -0
  190. package/dist/useDateRangePicker.mjs +196 -0
  191. package/dist/useDateRangePicker.module.js +196 -0
  192. package/dist/useDateRangePicker.module.js.map +1 -0
  193. package/dist/useDateSegment.main.js +373 -0
  194. package/dist/useDateSegment.main.js.map +1 -0
  195. package/dist/useDateSegment.mjs +364 -0
  196. package/dist/useDateSegment.module.js +364 -0
  197. package/dist/useDateSegment.module.js.map +1 -0
  198. package/dist/useDisplayNames.main.js +59 -0
  199. package/dist/useDisplayNames.main.js.map +1 -0
  200. package/dist/useDisplayNames.mjs +54 -0
  201. package/dist/useDisplayNames.module.js +54 -0
  202. package/dist/useDisplayNames.module.js.map +1 -0
  203. package/dist/zh-CN.main.js +21 -0
  204. package/dist/zh-CN.main.js.map +1 -0
  205. package/dist/zh-CN.mjs +23 -0
  206. package/dist/zh-CN.module.js +23 -0
  207. package/dist/zh-CN.module.js.map +1 -0
  208. package/dist/zh-TW.main.js +21 -0
  209. package/dist/zh-TW.main.js.map +1 -0
  210. package/dist/zh-TW.mjs +23 -0
  211. package/dist/zh-TW.module.js +23 -0
  212. package/dist/zh-TW.module.js.map +1 -0
  213. package/package.json +27 -18
  214. package/src/index.ts +12 -5
  215. package/src/useDateField.ts +163 -32
  216. package/src/useDatePicker.ts +113 -26
  217. package/src/useDatePickerGroup.ts +71 -13
  218. package/src/useDateRangePicker.ts +149 -38
  219. package/src/useDateSegment.ts +145 -91
  220. package/src/useDisplayNames.ts +10 -8
@@ -10,30 +10,34 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {DatePickerFieldState, DateSegment} from '@react-stately/datepicker';
14
- import {DatePickerProps, DateValue} from '@react-types/datepicker';
15
- import {DOMProps} from '@react-types/shared';
16
- import {getScrollParent, isIOS, isMac, mergeProps, scrollIntoView, useEvent, useId} from '@react-aria/utils';
17
- import {labelIds} from './useDateField';
13
+ import {CalendarDate, toCalendar} from '@internationalized/date';
14
+ import {DateFieldState, DateSegment} from '@react-stately/datepicker';
15
+ import {getScrollParent, isIOS, isMac, mergeProps, scrollIntoViewport, useEvent, useId, useLabels, useLayoutEffect} from '@react-aria/utils';
16
+ import {hookData} from './useDateField';
18
17
  import {NumberParser} from '@internationalized/number';
19
- import React, {HTMLAttributes, RefObject, useMemo, useRef} from 'react';
18
+ import React, {useMemo, useRef} from 'react';
19
+ import {RefObject} from '@react-types/shared';
20
20
  import {useDateFormatter, useFilter, useLocale} from '@react-aria/i18n';
21
21
  import {useDisplayNames} from './useDisplayNames';
22
- import {useFocusManager} from '@react-aria/focus';
23
- import {usePress} from '@react-aria/interactions';
24
22
  import {useSpinButton} from '@react-aria/spinbutton';
25
23
 
26
- interface DateSegmentAria {
27
- segmentProps: HTMLAttributes<HTMLDivElement>
24
+ export interface DateSegmentAria {
25
+ /** Props for the segment element. */
26
+ segmentProps: React.HTMLAttributes<HTMLDivElement>
28
27
  }
29
28
 
30
- export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> & DOMProps, segment: DateSegment, state: DatePickerFieldState, ref: RefObject<HTMLElement>): DateSegmentAria {
29
+ /**
30
+ * Provides the behavior and accessibility implementation for a segment in a date field.
31
+ * A date segment displays an individual unit of a date and time, and allows users to edit
32
+ * the value by typing or using the arrow keys to increment and decrement.
33
+ */
34
+ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref: RefObject<HTMLElement | null>): DateSegmentAria {
31
35
  let enteredKeys = useRef('');
32
- let {locale, direction} = useLocale();
36
+ let {locale} = useLocale();
33
37
  let displayNames = useDisplayNames();
34
- let focusManager = useFocusManager();
38
+ let {ariaLabel, ariaLabelledBy, ariaDescribedBy, focusManager} = hookData.get(state);
35
39
 
36
- let textValue = segment.text;
40
+ let textValue = segment.isPlaceholder ? '' : segment.text;
37
41
  let options = useMemo(() => state.dateFormatter.resolvedOptions(), [state.dateFormatter]);
38
42
  let monthDateFormatter = useDateFormatter({month: 'long', timeZone: options.timeZone});
39
43
  let hourDateFormatter = useDateFormatter({
@@ -42,21 +46,24 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
42
46
  timeZone: options.timeZone
43
47
  });
44
48
 
45
- if (segment.type === 'month') {
49
+ if (segment.type === 'month' && !segment.isPlaceholder) {
46
50
  let monthTextValue = monthDateFormatter.format(state.dateValue);
47
51
  textValue = monthTextValue !== textValue ? `${textValue} – ${monthTextValue}` : monthTextValue;
48
- } else if (segment.type === 'hour' || segment.type === 'dayPeriod') {
52
+ } else if (segment.type === 'hour' && !segment.isPlaceholder) {
49
53
  textValue = hourDateFormatter.format(state.dateValue);
50
54
  }
51
55
 
52
56
  let {spinButtonProps} = useSpinButton({
57
+ // The ARIA spec says aria-valuenow is optional if there's no value, but aXe seems to require it.
58
+ // This doesn't seem to have any negative effects with real AT since we also use aria-valuetext.
59
+ // https://github.com/dequelabs/axe-core/issues/3505
53
60
  value: segment.value,
54
61
  textValue,
55
62
  minValue: segment.minValue,
56
63
  maxValue: segment.maxValue,
57
- isDisabled: props.isDisabled,
58
- isReadOnly: props.isReadOnly || !segment.isEditable,
59
- isRequired: props.isRequired,
64
+ isDisabled: state.isDisabled,
65
+ isReadOnly: state.isReadOnly || !segment.isEditable,
66
+ isRequired: state.isRequired,
60
67
  onIncrement: () => {
61
68
  enteredKeys.current = '';
62
69
  state.increment(segment.type);
@@ -86,9 +93,13 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
86
93
  let parser = useMemo(() => new NumberParser(locale, {maximumFractionDigits: 0}), [locale]);
87
94
 
88
95
  let backspace = () => {
89
- if (parser.isValidPartialNumber(segment.text) && !props.isReadOnly && !segment.isPlaceholder) {
96
+ if (segment.text === segment.placeholder) {
97
+ focusManager.focusPrevious();
98
+ }
99
+ if (parser.isValidPartialNumber(segment.text) && !state.isReadOnly && !segment.isPlaceholder) {
90
100
  let newValue = segment.text.slice(0, -1);
91
101
  let parsed = parser.parse(newValue);
102
+ newValue = parsed === 0 ? '' : newValue;
92
103
  if (newValue.length === 0 || parsed === 0) {
93
104
  state.clearSegment(segment.type);
94
105
  } else {
@@ -112,34 +123,6 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
112
123
  }
113
124
 
114
125
  switch (e.key) {
115
- case 'ArrowLeft':
116
- e.preventDefault();
117
- e.stopPropagation();
118
- if (direction === 'rtl') {
119
- focusManager.focusNext();
120
- } else {
121
- focusManager.focusPrevious();
122
- }
123
- break;
124
- case 'ArrowRight':
125
- e.preventDefault();
126
- e.stopPropagation();
127
- if (direction === 'rtl') {
128
- focusManager.focusPrevious();
129
- } else {
130
- focusManager.focusNext();
131
- }
132
- break;
133
- case 'Enter':
134
- e.preventDefault();
135
- e.stopPropagation();
136
- if (segment.isPlaceholder && !props.isReadOnly) {
137
- state.confirmPlaceholder(segment.type);
138
- }
139
- focusManager.focusNext();
140
- break;
141
- case 'Tab':
142
- break;
143
126
  case 'Backspace':
144
127
  case 'Delete': {
145
128
  // Safari on iOS does not fire beforeinput for the backspace key because the cursor is at the start.
@@ -166,8 +149,36 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
166
149
  return amPmFormatter.formatToParts(date).find(part => part.type === 'dayPeriod').value;
167
150
  }, [amPmFormatter]);
168
151
 
152
+ // Get a list of formatted era names so users can type the first character to choose one.
153
+ let eraFormatter = useDateFormatter({year: 'numeric', era: 'narrow', timeZone: 'UTC'});
154
+ let eras = useMemo(() => {
155
+ if (segment.type !== 'era') {
156
+ return [];
157
+ }
158
+
159
+ let date = toCalendar(new CalendarDate(1, 1, 1), state.calendar);
160
+ let eras = state.calendar.getEras().map(era => {
161
+ let eraDate = date.set({year: 1, month: 1, day: 1, era}).toDate('UTC');
162
+ let parts = eraFormatter.formatToParts(eraDate);
163
+ let formatted = parts.find(p => p.type === 'era').value;
164
+ return {era, formatted};
165
+ });
166
+
167
+ // Remove the common prefix from formatted values. This is so that in calendars with eras like
168
+ // ERA0 and ERA1 (e.g. Ethiopic), users can press "0" and "1" to select an era. In other cases,
169
+ // the first letter is used.
170
+ let prefixLength = commonPrefixLength(eras.map(era => era.formatted));
171
+ if (prefixLength) {
172
+ for (let era of eras) {
173
+ era.formatted = era.formatted.slice(prefixLength);
174
+ }
175
+ }
176
+
177
+ return eras;
178
+ }, [eraFormatter, state.calendar, segment.type]);
179
+
169
180
  let onInput = (key: string) => {
170
- if (props.isDisabled || props.isReadOnly) {
181
+ if (state.isDisabled || state.isReadOnly) {
171
182
  return;
172
183
  }
173
184
 
@@ -184,6 +195,14 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
184
195
  }
185
196
  focusManager.focusNext();
186
197
  break;
198
+ case 'era': {
199
+ let matched = eras.find(e => startsWith(e.formatted, key));
200
+ if (matched) {
201
+ state.setSegment('era', matched.era);
202
+ focusManager.focusNext();
203
+ }
204
+ break;
205
+ }
187
206
  case 'day':
188
207
  case 'hour':
189
208
  case 'minute':
@@ -243,16 +262,24 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
243
262
 
244
263
  let onFocus = () => {
245
264
  enteredKeys.current = '';
246
- scrollIntoView(getScrollParent(ref.current) as HTMLElement, ref.current);
265
+ scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
247
266
 
248
- // Safari requires that a selection is set or it won't fire input events.
249
- // Since usePress disables text selection, this won't happen by default.
250
- ref.current.style.webkitUserSelect = 'text';
267
+ // Collapse selection to start or Chrome won't fire input events.
251
268
  let selection = window.getSelection();
252
269
  selection.collapse(ref.current);
253
- ref.current.style.webkitUserSelect = '';
254
270
  };
255
271
 
272
+ let documentRef = useRef(typeof document !== 'undefined' ? document : null);
273
+ useEvent(documentRef, 'selectionchange', () => {
274
+ // Enforce that the selection is collapsed when inside a date segment.
275
+ // Otherwise, when tapping on a segment in Android Chrome and then entering text,
276
+ // composition events will be fired that break the DOM structure and crash the page.
277
+ let selection = window.getSelection();
278
+ if (ref.current.contains(selection.anchorNode)) {
279
+ selection.collapse(ref.current);
280
+ }
281
+ });
282
+
256
283
  let compositionRef = useRef('');
257
284
  // @ts-ignore - TODO: possibly old TS version? doesn't fail in my editor...
258
285
  useEvent(ref, 'beforeinput', e => {
@@ -261,7 +288,7 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
261
288
  switch (e.inputType) {
262
289
  case 'deleteContentBackward':
263
290
  case 'deleteContentForward':
264
- if (parser.isValidPartialNumber(segment.text) && !props.isReadOnly) {
291
+ if (parser.isValidPartialNumber(segment.text) && !state.isReadOnly) {
265
292
  backspace();
266
293
  }
267
294
  break;
@@ -298,26 +325,18 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
298
325
  }
299
326
  });
300
327
 
301
- // Focus on mouse down/touch up to match native textfield behavior.
302
- // usePress handles canceling text selection.
303
- let {pressProps} = usePress({
304
- preventFocusOnPress: true,
305
- onPressStart: (e) => {
306
- if (e.pointerType === 'mouse') {
307
- e.target.focus();
308
- }
309
- },
310
- onPress(e) {
311
- if (e.pointerType !== 'mouse') {
312
- e.target.focus();
328
+ useLayoutEffect(() => {
329
+ let element = ref.current;
330
+ return () => {
331
+ // If the focused segment is removed, focus the previous one, or the next one if there was no previous one.
332
+ if (document.activeElement === element) {
333
+ let prev = focusManager.focusPrevious();
334
+ if (!prev) {
335
+ focusManager.focusNext();
336
+ }
313
337
  }
314
- }
315
- });
316
-
317
- // For Android: prevent selection on long press.
318
- useEvent(ref, 'selectstart', e => {
319
- e.preventDefault();
320
- });
338
+ };
339
+ }, [ref, focusManager]);
321
340
 
322
341
  // spinbuttons cannot be focused with VoiceOver on iOS.
323
342
  let touchPropOverrides = isIOS() || segment.type === 'timeZoneName' ? {
@@ -328,40 +347,75 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
328
347
  'aria-valuenow': null
329
348
  } : {};
330
349
 
331
- let {ariaLabelledBy, ariaDescribedBy} = labelIds.get(state);
332
-
333
350
  // Only apply aria-describedby to the first segment, unless the field is invalid. This avoids it being
334
351
  // read every time the user navigates to a new segment.
335
352
  let firstSegment = useMemo(() => state.segments.find(s => s.isEditable), [state.segments]);
336
- if (segment !== firstSegment && state.validationState !== 'invalid') {
353
+ if (segment !== firstSegment && !state.isInvalid) {
337
354
  ariaDescribedBy = undefined;
338
355
  }
339
356
 
340
- let id = useId(props.id);
341
- let isEditable = !props.isDisabled && !props.isReadOnly && segment.isEditable;
357
+ let id = useId();
358
+ let isEditable = !state.isDisabled && !state.isReadOnly && segment.isEditable;
359
+
360
+ // Prepend the label passed from the field to each segment name.
361
+ // This is needed because VoiceOver on iOS does not announce groups.
362
+ let name = segment.type === 'literal' ? '' : displayNames.of(segment.type);
363
+ let labelProps = useLabels({
364
+ 'aria-label': `${name}${ariaLabel ? `, ${ariaLabel}` : ''}${ariaLabelledBy ? ', ' : ''}`,
365
+ 'aria-labelledby': ariaLabelledBy
366
+ });
367
+
368
+ // Literal segments should not be visible to screen readers. We don't really need any of the above,
369
+ // but the rules of hooks mean hooks cannot be conditional so we have to put this condition here.
370
+ if (segment.type === 'literal') {
371
+ return {
372
+ segmentProps: {
373
+ 'aria-hidden': true
374
+ }
375
+ };
376
+ }
377
+
342
378
  return {
343
- segmentProps: mergeProps(spinButtonProps, pressProps, {
379
+ segmentProps: mergeProps(spinButtonProps, labelProps, {
344
380
  id,
345
381
  ...touchPropOverrides,
346
- 'aria-controls': props['aria-controls'],
347
- // 'aria-haspopup': props['aria-haspopup'], // deprecated in ARIA 1.2
348
- 'aria-invalid': state.validationState === 'invalid' ? 'true' : undefined,
349
- 'aria-label': segment.type !== 'literal' ? displayNames.of(segment.type) : undefined,
350
- 'aria-labelledby': `${ariaLabelledBy} ${id}`,
382
+ 'aria-invalid': state.isInvalid ? 'true' : undefined,
351
383
  'aria-describedby': ariaDescribedBy,
352
- 'aria-placeholder': segment.isPlaceholder ? segment.text : undefined,
353
- 'aria-readonly': props.isReadOnly || !segment.isEditable ? 'true' : undefined,
384
+ 'aria-readonly': state.isReadOnly || !segment.isEditable ? 'true' : undefined,
385
+ 'data-placeholder': segment.isPlaceholder || undefined,
354
386
  contentEditable: isEditable,
355
387
  suppressContentEditableWarning: isEditable,
356
388
  spellCheck: isEditable ? 'false' : undefined,
357
- autoCapitalize: isEditable ? 'off' : undefined,
358
389
  autoCorrect: isEditable ? 'off' : undefined,
359
390
  // Capitalization was changed in React 17...
360
391
  [parseInt(React.version, 10) >= 17 ? 'enterKeyHint' : 'enterkeyhint']: isEditable ? 'next' : undefined,
361
- inputMode: props.isDisabled || segment.type === 'dayPeriod' || !isEditable ? undefined : 'numeric',
362
- tabIndex: props.isDisabled ? undefined : 0,
392
+ inputMode: state.isDisabled || segment.type === 'dayPeriod' || segment.type === 'era' || !isEditable ? undefined : 'numeric',
393
+ tabIndex: state.isDisabled ? undefined : 0,
363
394
  onKeyDown,
364
- onFocus
395
+ onFocus,
396
+ style: {
397
+ caretColor: 'transparent'
398
+ },
399
+ // Prevent pointer events from reaching useDatePickerGroup, and allow native browser behavior to focus the segment.
400
+ onPointerDown(e) {
401
+ e.stopPropagation();
402
+ },
403
+ onMouseDown(e) {
404
+ e.stopPropagation();
405
+ }
365
406
  })
366
407
  };
367
408
  }
409
+
410
+ function commonPrefixLength(strings: string[]): number {
411
+ // Sort the strings, and compare the characters in the first and last to find the common prefix.
412
+ strings.sort();
413
+ let first = strings[0];
414
+ let last = strings[strings.length - 1];
415
+ for (let i = 0; i < first.length; i++) {
416
+ if (first[i] !== last[i]) {
417
+ return i;
418
+ }
419
+ }
420
+ return 0;
421
+ }
@@ -12,17 +12,19 @@
12
12
 
13
13
  // @ts-ignore
14
14
  import intlMessages from '../intl/*.json';
15
- import {MessageDictionary} from '@internationalized/message';
16
- import {useLocale} from '@react-aria/i18n';
15
+ import {LocalizedStringDictionary} from '@internationalized/string';
16
+ import {useLocale, useLocalizedStringDictionary} from '@react-aria/i18n';
17
17
  import {useMemo} from 'react';
18
18
 
19
- type Field = 'era' | 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'dayPeriod' | 'timeZoneName' | 'weekday';
19
+ type Field = Intl.DateTimeFormatPartTypes;
20
20
  interface DisplayNames {
21
21
  of(field: Field): string
22
22
  }
23
23
 
24
+ /** @private */
24
25
  export function useDisplayNames(): DisplayNames {
25
26
  let {locale} = useLocale();
27
+ let dictionary = useLocalizedStringDictionary(intlMessages, '@react-aria/datepicker');
26
28
  return useMemo(() => {
27
29
  // Try to use Intl.DisplayNames if possible. It may be supported in browsers, but not support the dateTimeField
28
30
  // type as that was only added in v2. https://github.com/tc39/intl-displaynames-v2
@@ -30,18 +32,18 @@ export function useDisplayNames(): DisplayNames {
30
32
  // @ts-ignore
31
33
  return new Intl.DisplayNames(locale, {type: 'dateTimeField'});
32
34
  } catch (err) {
33
- return new DisplayNamesPolyfill(locale);
35
+ return new DisplayNamesPolyfill(locale, dictionary);
34
36
  }
35
- }, [locale]);
37
+ }, [locale, dictionary]);
36
38
  }
37
39
 
38
40
  class DisplayNamesPolyfill implements DisplayNames {
39
41
  private locale: string;
40
- private dictionary: MessageDictionary;
42
+ private dictionary: LocalizedStringDictionary<Field, string>;
41
43
 
42
- constructor(locale: string) {
44
+ constructor(locale: string, dictionary: LocalizedStringDictionary<Field, string>) {
43
45
  this.locale = locale;
44
- this.dictionary = new MessageDictionary(intlMessages);
46
+ this.dictionary = dictionary;
45
47
  }
46
48
 
47
49
  of(field: Field): string {