@react-aria/datepicker 3.0.0-alpha.4 → 3.0.0-alpha.5

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.
@@ -10,28 +10,31 @@
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 {DateFieldState, DateSegment} from '@react-stately/datepicker';
14
+ import {getScrollParent, isIOS, isMac, mergeProps, scrollIntoView, useEvent, useId, useLabels} from '@react-aria/utils';
15
+ import {hookData} from './useDateField';
18
16
  import {NumberParser} from '@internationalized/number';
19
17
  import React, {HTMLAttributes, RefObject, useMemo, useRef} from 'react';
20
18
  import {useDateFormatter, useFilter, useLocale} from '@react-aria/i18n';
21
19
  import {useDisplayNames} from './useDisplayNames';
22
- import {useFocusManager} from '@react-aria/focus';
23
20
  import {usePress} from '@react-aria/interactions';
24
21
  import {useSpinButton} from '@react-aria/spinbutton';
25
22
 
26
23
  interface DateSegmentAria {
24
+ /** Props for the segment element. */
27
25
  segmentProps: HTMLAttributes<HTMLDivElement>
28
26
  }
29
27
 
30
- export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> & DOMProps, segment: DateSegment, state: DatePickerFieldState, ref: RefObject<HTMLElement>): DateSegmentAria {
28
+ /**
29
+ * Provides the behavior and accessibility implementation for a segment in a date field.
30
+ * A date segment displays an individual unit of a date and time, and allows users to edit
31
+ * the value by typing or using the arrow keys to increment and decrement.
32
+ */
33
+ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref: RefObject<HTMLElement>): DateSegmentAria {
31
34
  let enteredKeys = useRef('');
32
35
  let {locale, direction} = useLocale();
33
36
  let displayNames = useDisplayNames();
34
- let focusManager = useFocusManager();
37
+ let {ariaLabel, ariaLabelledBy, ariaDescribedBy, focusManager} = hookData.get(state);
35
38
 
36
39
  let textValue = segment.text;
37
40
  let options = useMemo(() => state.dateFormatter.resolvedOptions(), [state.dateFormatter]);
@@ -45,7 +48,7 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
45
48
  if (segment.type === 'month') {
46
49
  let monthTextValue = monthDateFormatter.format(state.dateValue);
47
50
  textValue = monthTextValue !== textValue ? `${textValue} – ${monthTextValue}` : monthTextValue;
48
- } else if (segment.type === 'hour' || segment.type === 'dayPeriod') {
51
+ } else if (segment.type === 'hour') {
49
52
  textValue = hourDateFormatter.format(state.dateValue);
50
53
  }
51
54
 
@@ -54,9 +57,9 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
54
57
  textValue,
55
58
  minValue: segment.minValue,
56
59
  maxValue: segment.maxValue,
57
- isDisabled: props.isDisabled,
58
- isReadOnly: props.isReadOnly || !segment.isEditable,
59
- isRequired: props.isRequired,
60
+ isDisabled: state.isDisabled,
61
+ isReadOnly: state.isReadOnly || !segment.isEditable,
62
+ isRequired: state.isRequired,
60
63
  onIncrement: () => {
61
64
  enteredKeys.current = '';
62
65
  state.increment(segment.type);
@@ -86,7 +89,7 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
86
89
  let parser = useMemo(() => new NumberParser(locale, {maximumFractionDigits: 0}), [locale]);
87
90
 
88
91
  let backspace = () => {
89
- if (parser.isValidPartialNumber(segment.text) && !props.isReadOnly && !segment.isPlaceholder) {
92
+ if (parser.isValidPartialNumber(segment.text) && !state.isReadOnly && !segment.isPlaceholder) {
90
93
  let newValue = segment.text.slice(0, -1);
91
94
  let parsed = parser.parse(newValue);
92
95
  if (newValue.length === 0 || parsed === 0) {
@@ -116,27 +119,27 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
116
119
  e.preventDefault();
117
120
  e.stopPropagation();
118
121
  if (direction === 'rtl') {
119
- focusManager.focusNext();
122
+ focusManager.focusNext({tabbable: true});
120
123
  } else {
121
- focusManager.focusPrevious();
124
+ focusManager.focusPrevious({tabbable: true});
122
125
  }
123
126
  break;
124
127
  case 'ArrowRight':
125
128
  e.preventDefault();
126
129
  e.stopPropagation();
127
130
  if (direction === 'rtl') {
128
- focusManager.focusPrevious();
131
+ focusManager.focusPrevious({tabbable: true});
129
132
  } else {
130
- focusManager.focusNext();
133
+ focusManager.focusNext({tabbable: true});
131
134
  }
132
135
  break;
133
136
  case 'Enter':
134
137
  e.preventDefault();
135
138
  e.stopPropagation();
136
- if (segment.isPlaceholder && !props.isReadOnly) {
139
+ if (segment.isPlaceholder && !state.isReadOnly) {
137
140
  state.confirmPlaceholder(segment.type);
138
141
  }
139
- focusManager.focusNext();
142
+ focusManager.focusNext({tabbable: true});
140
143
  break;
141
144
  case 'Tab':
142
145
  break;
@@ -167,7 +170,7 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
167
170
  }, [amPmFormatter]);
168
171
 
169
172
  let onInput = (key: string) => {
170
- if (props.isDisabled || props.isReadOnly) {
173
+ if (state.isDisabled || state.isReadOnly) {
171
174
  return;
172
175
  }
173
176
 
@@ -182,7 +185,7 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
182
185
  } else {
183
186
  break;
184
187
  }
185
- focusManager.focusNext();
188
+ focusManager.focusNext({tabbable: true});
186
189
  break;
187
190
  case 'day':
188
191
  case 'hour':
@@ -231,7 +234,7 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
231
234
  if (Number(numberValue + '0') > segment.maxValue || newValue.length >= String(segment.maxValue).length) {
232
235
  enteredKeys.current = '';
233
236
  if (shouldSetValue) {
234
- focusManager.focusNext();
237
+ focusManager.focusNext({tabbable: true});
235
238
  }
236
239
  } else {
237
240
  enteredKeys.current = newValue;
@@ -261,7 +264,7 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
261
264
  switch (e.inputType) {
262
265
  case 'deleteContentBackward':
263
266
  case 'deleteContentForward':
264
- if (parser.isValidPartialNumber(segment.text) && !props.isReadOnly) {
267
+ if (parser.isValidPartialNumber(segment.text) && !state.isReadOnly) {
265
268
  backspace();
266
269
  }
267
270
  break;
@@ -328,8 +331,6 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
328
331
  'aria-valuenow': null
329
332
  } : {};
330
333
 
331
- let {ariaLabelledBy, ariaDescribedBy} = labelIds.get(state);
332
-
333
334
  // Only apply aria-describedby to the first segment, unless the field is invalid. This avoids it being
334
335
  // read every time the user navigates to a new segment.
335
336
  let firstSegment = useMemo(() => state.segments.find(s => s.isEditable), [state.segments]);
@@ -337,20 +338,35 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
337
338
  ariaDescribedBy = undefined;
338
339
  }
339
340
 
340
- let id = useId(props.id);
341
- let isEditable = !props.isDisabled && !props.isReadOnly && segment.isEditable;
341
+ let id = useId();
342
+ let isEditable = !state.isDisabled && !state.isReadOnly && segment.isEditable;
343
+
344
+ // Prepend the label passed from the field to each segment name.
345
+ // This is needed because VoiceOver on iOS does not announce groups.
346
+ let name = segment.type === 'literal' ? '' : displayNames.of(segment.type);
347
+ let labelProps = useLabels({
348
+ 'aria-label': (ariaLabel ? ariaLabel + ' ' : '') + name,
349
+ 'aria-labelledby': ariaLabelledBy
350
+ });
351
+
352
+ // Literal segments should not be visible to screen readers. We don't really need any of the above,
353
+ // but the rules of hooks mean hooks cannot be conditional so we have to put this condition here.
354
+ if (segment.type === 'literal') {
355
+ return {
356
+ segmentProps: {
357
+ 'aria-hidden': true
358
+ }
359
+ };
360
+ }
361
+
342
362
  return {
343
- segmentProps: mergeProps(spinButtonProps, pressProps, {
363
+ segmentProps: mergeProps(spinButtonProps, pressProps, labelProps, {
344
364
  id,
345
365
  ...touchPropOverrides,
346
- 'aria-controls': props['aria-controls'],
347
- // 'aria-haspopup': props['aria-haspopup'], // deprecated in ARIA 1.2
348
366
  'aria-invalid': state.validationState === 'invalid' ? 'true' : undefined,
349
- 'aria-label': segment.type !== 'literal' ? displayNames.of(segment.type) : undefined,
350
- 'aria-labelledby': `${ariaLabelledBy} ${id}`,
351
367
  'aria-describedby': ariaDescribedBy,
352
368
  'aria-placeholder': segment.isPlaceholder ? segment.text : undefined,
353
- 'aria-readonly': props.isReadOnly || !segment.isEditable ? 'true' : undefined,
369
+ 'aria-readonly': state.isReadOnly || !segment.isEditable ? 'true' : undefined,
354
370
  contentEditable: isEditable,
355
371
  suppressContentEditableWarning: isEditable,
356
372
  spellCheck: isEditable ? 'false' : undefined,
@@ -358,10 +374,13 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
358
374
  autoCorrect: isEditable ? 'off' : undefined,
359
375
  // Capitalization was changed in React 17...
360
376
  [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,
377
+ inputMode: state.isDisabled || segment.type === 'dayPeriod' || !isEditable ? undefined : 'numeric',
378
+ tabIndex: state.isDisabled ? undefined : 0,
363
379
  onKeyDown,
364
- onFocus
380
+ onFocus,
381
+ style: {
382
+ caretColor: 'transparent'
383
+ }
365
384
  })
366
385
  };
367
386
  }