@react-aria/datepicker 3.0.0-nightly.3180 → 3.0.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-aria/datepicker",
3
- "version": "3.0.0-nightly.3180+0bba35ae3",
3
+ "version": "3.0.0",
4
4
  "description": "Spectrum UI components in React",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/main.js",
@@ -18,27 +18,28 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@babel/runtime": "^7.6.2",
21
- "@internationalized/message": "3.0.6-nightly.3180+0bba35ae3",
22
- "@internationalized/number": "3.0.6-nightly.3180+0bba35ae3",
23
- "@react-aria/focus": "3.0.0-nightly.1481+0bba35ae3",
24
- "@react-aria/i18n": "3.0.0-nightly.1481+0bba35ae3",
25
- "@react-aria/interactions": "3.0.0-nightly.1481+0bba35ae3",
26
- "@react-aria/label": "3.0.0-nightly.1481+0bba35ae3",
27
- "@react-aria/spinbutton": "3.0.0-nightly.1481+0bba35ae3",
28
- "@react-aria/utils": "3.0.0-nightly.1481+0bba35ae3",
29
- "@react-stately/datepicker": "3.0.0-nightly.3180+0bba35ae3",
30
- "@react-types/button": "3.4.5-nightly.3180+0bba35ae3",
31
- "@react-types/calendar": "3.0.0-nightly.3180+0bba35ae3",
32
- "@react-types/datepicker": "3.0.0-nightly.3180+0bba35ae3",
33
- "@react-types/dialog": "3.3.5-nightly.3180+0bba35ae3",
34
- "@react-types/shared": "3.0.0-nightly.1481+0bba35ae3"
21
+ "@internationalized/date": "^3.0.0",
22
+ "@internationalized/message": "^3.0.8",
23
+ "@internationalized/number": "^3.1.1",
24
+ "@react-aria/focus": "^3.6.1",
25
+ "@react-aria/i18n": "^3.4.1",
26
+ "@react-aria/interactions": "^3.9.1",
27
+ "@react-aria/label": "^3.3.1",
28
+ "@react-aria/spinbutton": "^3.1.1",
29
+ "@react-aria/utils": "^3.13.1",
30
+ "@react-stately/datepicker": "^3.0.0",
31
+ "@react-types/button": "^3.5.1",
32
+ "@react-types/calendar": "^3.0.0",
33
+ "@react-types/datepicker": "^3.0.0",
34
+ "@react-types/dialog": "^3.4.1",
35
+ "@react-types/shared": "^3.13.1"
35
36
  },
36
37
  "peerDependencies": {
37
- "react": "^16.8.0 || ^17.0.0-rc.1",
38
- "react-dom": "^16.8.0 || ^17.0.0-rc.1"
38
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
39
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
39
40
  },
40
41
  "publishConfig": {
41
42
  "access": "public"
42
43
  },
43
- "gitHead": "0bba35ae36b5d220570385215860d3ca3b549656"
44
+ "gitHead": "715c3f563ccf8c2e0102d3e18403d9db21a05a71"
44
45
  }
package/src/index.ts CHANGED
@@ -14,4 +14,10 @@ export {useDatePicker} from './useDatePicker';
14
14
  export {useDateSegment} from './useDateSegment';
15
15
  export {useDateField, useTimeField} from './useDateField';
16
16
  export {useDateRangePicker} from './useDateRangePicker';
17
- export * from './useDisplayNames';
17
+ export {useDisplayNames} from './useDisplayNames';
18
+
19
+ export type {AriaDatePickerProps, AriaDateRangePickerProps} from '@react-types/datepicker';
20
+ export type {AriaDateFieldProps, DateFieldAria} from './useDateField';
21
+ export type {DatePickerAria} from './useDatePicker';
22
+ export type {DateRangePickerAria} from './useDateRangePicker';
23
+ export type {DateSegmentAria} from './useDateSegment';
@@ -13,16 +13,19 @@
13
13
  import {AriaDatePickerProps, AriaTimeFieldProps, DateValue, TimeValue} from '@react-types/datepicker';
14
14
  import {createFocusManager, FocusManager} from '@react-aria/focus';
15
15
  import {DateFieldState} from '@react-stately/datepicker';
16
+ import {filterDOMProps, mergeProps, useDescription} from '@react-aria/utils';
16
17
  import {HTMLAttributes, RefObject, useEffect, useMemo, useRef} from 'react';
17
- import {mergeProps, useDescription} from '@react-aria/utils';
18
+ // @ts-ignore
19
+ import intlMessages from '../intl/*.json';
18
20
  import {useDatePickerGroup} from './useDatePickerGroup';
19
21
  import {useField} from '@react-aria/label';
20
22
  import {useFocusWithin} from '@react-aria/interactions';
23
+ import {useMessageFormatter} from '@react-aria/i18n';
21
24
 
22
25
  // Allows this hook to also be used with TimeField
23
- interface DateFieldProps<T extends DateValue> extends Omit<AriaDatePickerProps<T>, 'value' | 'defaultValue' | 'onChange' | 'minValue' | 'maxValue' | 'placeholderValue'> {}
26
+ export interface AriaDateFieldProps<T extends DateValue> extends Omit<AriaDatePickerProps<T>, 'value' | 'defaultValue' | 'onChange' | 'minValue' | 'maxValue' | 'placeholderValue'> {}
24
27
 
25
- interface DateFieldAria {
28
+ export interface DateFieldAria {
26
29
  /** Props for the field's visible label element, if any. */
27
30
  labelProps: HTMLAttributes<HTMLElement>,
28
31
  /** Props for the field grouping element. */
@@ -53,21 +56,23 @@ export const focusManagerSymbol = '__focusManager_' + Date.now();
53
56
  * A date field allows users to enter and edit date and time values using a keyboard.
54
57
  * Each part of a date value is displayed in an individually editable segment.
55
58
  */
56
- export function useDateField<T extends DateValue>(props: DateFieldProps<T>, state: DateFieldState, ref: RefObject<HTMLElement>): DateFieldAria {
59
+ export function useDateField<T extends DateValue>(props: AriaDateFieldProps<T>, state: DateFieldState, ref: RefObject<HTMLElement>): DateFieldAria {
57
60
  let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({
58
61
  ...props,
59
62
  labelElementType: 'span'
60
63
  });
61
64
 
62
- let groupProps = useDatePickerGroup(state, ref);
63
-
64
65
  let {focusWithinProps} = useFocusWithin({
65
66
  onBlurWithin() {
66
67
  state.confirmPlaceholder();
67
68
  }
68
69
  });
69
70
 
70
- let descProps = useDescription(state.formatValue({month: 'long'}));
71
+ let formatMessage = useMessageFormatter(intlMessages);
72
+ let message = state.maxGranularity === 'hour' ? 'selectedTimeDescription' : 'selectedDateDescription';
73
+ let field = state.maxGranularity === 'hour' ? 'time' : 'date';
74
+ let description = state.value ? formatMessage(message, {[field]: state.formatValue({month: 'long'})}) : '';
75
+ let descProps = useDescription(description);
71
76
 
72
77
  // If within a date picker or date range picker, the date field will have role="presentation" and an aria-describedby
73
78
  // will be passed in that references the value (e.g. entire range). Otherwise, add the field's value description.
@@ -76,6 +81,7 @@ export function useDateField<T extends DateValue>(props: DateFieldProps<T>, stat
76
81
  : [descProps['aria-describedby'], fieldProps['aria-describedby']].filter(Boolean).join(' ') || undefined;
77
82
  let propsFocusManager = props[focusManagerSymbol];
78
83
  let focusManager = useMemo(() => propsFocusManager || createFocusManager(ref), [propsFocusManager, ref]);
84
+ let groupProps = useDatePickerGroup(state, ref, props[roleSymbol] === 'presentation');
79
85
 
80
86
  // Pass labels and other information to segments.
81
87
  hookData.set(state, {
@@ -111,6 +117,7 @@ export function useDateField<T extends DateValue>(props: DateFieldProps<T>, stat
111
117
  autoFocusRef.current = false;
112
118
  }, [focusManager]);
113
119
 
120
+ let domProps = filterDOMProps(props);
114
121
  return {
115
122
  labelProps: {
116
123
  ...labelProps,
@@ -118,7 +125,7 @@ export function useDateField<T extends DateValue>(props: DateFieldProps<T>, stat
118
125
  focusManager.focusFirst();
119
126
  }
120
127
  },
121
- fieldProps: mergeProps(fieldDOMProps, groupProps, focusWithinProps),
128
+ fieldProps: mergeProps(domProps, fieldDOMProps, groupProps, focusWithinProps),
122
129
  descriptionProps,
123
130
  errorMessageProps
124
131
  };
@@ -16,16 +16,16 @@ import {AriaDialogProps} from '@react-types/dialog';
16
16
  import {CalendarProps} from '@react-types/calendar';
17
17
  import {createFocusManager} from '@react-aria/focus';
18
18
  import {DatePickerState} from '@react-stately/datepicker';
19
- import {HTMLAttributes, RefObject} from 'react';
19
+ import {filterDOMProps, mergeProps, useDescription, useId} from '@react-aria/utils';
20
+ import {HTMLAttributes, RefObject, useMemo} from 'react';
20
21
  // @ts-ignore
21
22
  import intlMessages from '../intl/*.json';
22
- import {mergeProps, useDescription, useId} from '@react-aria/utils';
23
23
  import {roleSymbol} from './useDateField';
24
24
  import {useDatePickerGroup} from './useDatePickerGroup';
25
25
  import {useField} from '@react-aria/label';
26
26
  import {useLocale, useMessageFormatter} from '@react-aria/i18n';
27
27
 
28
- interface DatePickerAria {
28
+ export interface DatePickerAria {
29
29
  /** Props for the date picker's visible label element, if any. */
30
30
  labelProps: HTMLAttributes<HTMLElement>,
31
31
  /** Props for the grouping element containing the date field and button. */
@@ -63,11 +63,15 @@ export function useDatePicker<T extends DateValue>(props: AriaDatePickerProps<T>
63
63
  let labelledBy = fieldProps['aria-labelledby'] || fieldProps.id;
64
64
 
65
65
  let {locale} = useLocale();
66
- let descProps = useDescription(state.formatValue(locale, {month: 'long'}));
66
+ let date = state.formatValue(locale, {month: 'long'});
67
+ let description = date ? formatMessage('selectedDateDescription', {date}) : '';
68
+ let descProps = useDescription(description);
67
69
  let ariaDescribedBy = [descProps['aria-describedby'], fieldProps['aria-describedby']].filter(Boolean).join(' ') || undefined;
70
+ let domProps = filterDOMProps(props);
71
+ let focusManager = useMemo(() => createFocusManager(ref), [ref]);
68
72
 
69
73
  return {
70
- groupProps: mergeProps(groupProps, fieldProps, descProps, {
74
+ groupProps: mergeProps(domProps, groupProps, fieldProps, descProps, {
71
75
  role: 'group',
72
76
  'aria-disabled': props.isDisabled || null,
73
77
  'aria-labelledby': labelledBy,
@@ -76,7 +80,6 @@ export function useDatePicker<T extends DateValue>(props: AriaDatePickerProps<T>
76
80
  labelProps: {
77
81
  ...labelProps,
78
82
  onClick: () => {
79
- let focusManager = createFocusManager(ref);
80
83
  focusManager.focusFirst();
81
84
  }
82
85
  },
@@ -103,7 +106,6 @@ export function useDatePicker<T extends DateValue>(props: AriaDatePickerProps<T>
103
106
  buttonProps: {
104
107
  ...descProps,
105
108
  id: buttonId,
106
- excludeFromTabOrder: true,
107
109
  'aria-haspopup': 'dialog',
108
110
  'aria-label': formatMessage('calendar'),
109
111
  'aria-labelledby': `${labelledBy} ${buttonId}`,
@@ -123,7 +125,9 @@ export function useDatePicker<T extends DateValue>(props: AriaDatePickerProps<T>
123
125
  isDisabled: props.isDisabled,
124
126
  isReadOnly: props.isReadOnly,
125
127
  isDateUnavailable: props.isDateUnavailable,
126
- defaultFocusedValue: state.dateValue ? undefined : props.placeholderValue
128
+ defaultFocusedValue: state.dateValue ? undefined : props.placeholderValue,
129
+ validationState: state.validationState,
130
+ errorMessage: props.errorMessage
127
131
  }
128
132
  };
129
133
  }
@@ -1,11 +1,15 @@
1
+ import {createFocusManager, getFocusableTreeWalker} from '@react-aria/focus';
1
2
  import {DateFieldState, DatePickerState, DateRangePickerState} from '@react-stately/datepicker';
2
- import {getFocusableTreeWalker} from '@react-aria/focus';
3
3
  import {KeyboardEvent} from '@react-types/shared';
4
4
  import {mergeProps} from '@react-aria/utils';
5
- import {RefObject} from 'react';
5
+ import {RefObject, useMemo} from 'react';
6
+ import {useLocale} from '@react-aria/i18n';
6
7
  import {usePress} from '@react-aria/interactions';
7
8
 
8
- export function useDatePickerGroup(state: DatePickerState | DateRangePickerState | DateFieldState, ref: RefObject<HTMLElement>) {
9
+ export function useDatePickerGroup(state: DatePickerState | DateRangePickerState | DateFieldState, ref: RefObject<HTMLElement>, disableArrowNavigation?: boolean) {
10
+ let {direction} = useLocale();
11
+ let focusManager = useMemo(() => createFocusManager(ref), [ref]);
12
+
9
13
  // Open the popover on alt + arrow down
10
14
  let onKeyDown = (e: KeyboardEvent) => {
11
15
  if (e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp') && 'setOpen' in state) {
@@ -13,6 +17,31 @@ export function useDatePickerGroup(state: DatePickerState | DateRangePickerState
13
17
  e.stopPropagation();
14
18
  state.setOpen(true);
15
19
  }
20
+
21
+ if (disableArrowNavigation) {
22
+ return;
23
+ }
24
+
25
+ switch (e.key) {
26
+ case 'ArrowLeft':
27
+ e.preventDefault();
28
+ e.stopPropagation();
29
+ if (direction === 'rtl') {
30
+ focusManager.focusNext();
31
+ } else {
32
+ focusManager.focusPrevious();
33
+ }
34
+ break;
35
+ case 'ArrowRight':
36
+ e.preventDefault();
37
+ e.stopPropagation();
38
+ if (direction === 'rtl') {
39
+ focusManager.focusPrevious();
40
+ } else {
41
+ focusManager.focusNext();
42
+ }
43
+ break;
44
+ }
16
45
  };
17
46
 
18
47
  // Focus the first placeholder segment from the end on mouse down/touch up in the field.
@@ -37,9 +66,9 @@ export function useDatePickerGroup(state: DatePickerState | DateRangePickerState
37
66
  }
38
67
 
39
68
  // Now go backwards until we find an element that is not a placeholder.
40
- while (target?.getAttribute('aria-placeholder')) {
69
+ while (target?.hasAttribute('data-placeholder')) {
41
70
  let prev = walker.previousNode() as HTMLElement;
42
- if (prev && prev.getAttribute('aria-placeholder')) {
71
+ if (prev && prev.hasAttribute('data-placeholder')) {
43
72
  target = prev;
44
73
  } else {
45
74
  break;
@@ -15,18 +15,17 @@ import {AriaDatePickerProps, AriaDateRangePickerProps, DateValue} from '@react-t
15
15
  import {AriaDialogProps} from '@react-types/dialog';
16
16
  import {createFocusManager} from '@react-aria/focus';
17
17
  import {DateRangePickerState} from '@react-stately/datepicker';
18
+ import {filterDOMProps, mergeProps, useDescription, useId} from '@react-aria/utils';
18
19
  import {focusManagerSymbol, roleSymbol} from './useDateField';
19
20
  import {HTMLAttributes, RefObject, useMemo} from 'react';
20
21
  // @ts-ignore
21
22
  import intlMessages from '../intl/*.json';
22
- import {mergeProps, useDescription, useId} from '@react-aria/utils';
23
23
  import {RangeCalendarProps} from '@react-types/calendar';
24
24
  import {useDatePickerGroup} from './useDatePickerGroup';
25
25
  import {useField} from '@react-aria/label';
26
- import {useFocusWithin} from '@react-aria/interactions';
27
26
  import {useLocale, useMessageFormatter} from '@react-aria/i18n';
28
27
 
29
- interface DateRangePickerAria {
28
+ export interface DateRangePickerAria {
30
29
  /** Props for the date range picker's visible label element, if any. */
31
30
  labelProps: HTMLAttributes<HTMLElement>,
32
31
  /** Props for the grouping element containing the date fields and button. */
@@ -62,7 +61,8 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
62
61
  let labelledBy = fieldProps['aria-labelledby'] || fieldProps.id;
63
62
 
64
63
  let {locale} = useLocale();
65
- let description = state.formatValue(locale, {month: 'long'});
64
+ let range = state.formatValue(locale, {month: 'long'});
65
+ let description = range ? formatMessage('selectedRangeDescription', {startDate: range.start, endDate: range.end}) : '';
66
66
  let descProps = useDescription(description);
67
67
 
68
68
  let startFieldProps = {
@@ -79,14 +79,13 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
79
79
  let dialogId = useId();
80
80
 
81
81
  let groupProps = useDatePickerGroup(state, ref);
82
- let {focusWithinProps} = useFocusWithin({
83
- onBlurWithin() {
84
- state.confirmPlaceholder();
85
- }
86
- });
87
82
 
88
83
  let ariaDescribedBy = [descProps['aria-describedby'], fieldProps['aria-describedby']].filter(Boolean).join(' ') || undefined;
89
- let focusManager = useMemo(() => createFocusManager(ref), [ref]);
84
+ let focusManager = useMemo(() => createFocusManager(ref, {
85
+ // Exclude the button from the focus manager.
86
+ accept: element => element.id !== buttonId
87
+ }), [ref, buttonId]);
88
+
90
89
  let commonFieldProps = {
91
90
  [focusManagerSymbol]: focusManager,
92
91
  [roleSymbol]: 'presentation',
@@ -103,8 +102,10 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
103
102
  validationState: state.validationState
104
103
  };
105
104
 
105
+ let domProps = filterDOMProps(props);
106
+
106
107
  return {
107
- groupProps: mergeProps(groupProps, fieldProps, descProps, focusWithinProps, {
108
+ groupProps: mergeProps(domProps, groupProps, fieldProps, descProps, {
108
109
  role: 'group',
109
110
  'aria-disabled': props.isDisabled || null,
110
111
  'aria-describedby': ariaDescribedBy
@@ -118,7 +119,6 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
118
119
  buttonProps: {
119
120
  ...descProps,
120
121
  id: buttonId,
121
- excludeFromTabOrder: true,
122
122
  'aria-haspopup': 'dialog',
123
123
  'aria-label': formatMessage('calendar'),
124
124
  'aria-labelledby': `${labelledBy} ${buttonId}`,
@@ -154,7 +154,9 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
154
154
  isReadOnly: props.isReadOnly,
155
155
  isDateUnavailable: props.isDateUnavailable,
156
156
  allowsNonContiguousRanges: props.allowsNonContiguousRanges,
157
- defaultFocusedValue: state.dateRange ? undefined : props.placeholderValue
157
+ defaultFocusedValue: state.dateRange ? undefined : props.placeholderValue,
158
+ validationState: state.validationState,
159
+ errorMessage: props.errorMessage
158
160
  }
159
161
  };
160
162
  }
@@ -10,17 +10,17 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import {CalendarDate, toCalendar} from '@internationalized/date';
13
14
  import {DateFieldState, DateSegment} from '@react-stately/datepicker';
14
- import {getScrollParent, isIOS, isMac, mergeProps, scrollIntoView, useEvent, useId, useLabels} from '@react-aria/utils';
15
+ import {getScrollParent, isIOS, isMac, mergeProps, scrollIntoView, useEvent, useId, useLabels, useLayoutEffect} from '@react-aria/utils';
15
16
  import {hookData} from './useDateField';
16
17
  import {NumberParser} from '@internationalized/number';
17
18
  import React, {HTMLAttributes, RefObject, useMemo, useRef} from 'react';
18
19
  import {useDateFormatter, useFilter, useLocale} from '@react-aria/i18n';
19
20
  import {useDisplayNames} from './useDisplayNames';
20
- import {usePress} from '@react-aria/interactions';
21
21
  import {useSpinButton} from '@react-aria/spinbutton';
22
22
 
23
- interface DateSegmentAria {
23
+ export interface DateSegmentAria {
24
24
  /** Props for the segment element. */
25
25
  segmentProps: HTMLAttributes<HTMLDivElement>
26
26
  }
@@ -32,11 +32,11 @@ interface DateSegmentAria {
32
32
  */
33
33
  export function useDateSegment(segment: DateSegment, state: DateFieldState, ref: RefObject<HTMLElement>): DateSegmentAria {
34
34
  let enteredKeys = useRef('');
35
- let {locale, direction} = useLocale();
35
+ let {locale} = useLocale();
36
36
  let displayNames = useDisplayNames();
37
37
  let {ariaLabel, ariaLabelledBy, ariaDescribedBy, focusManager} = hookData.get(state);
38
38
 
39
- let textValue = segment.text;
39
+ let textValue = segment.isPlaceholder ? '' : segment.text;
40
40
  let options = useMemo(() => state.dateFormatter.resolvedOptions(), [state.dateFormatter]);
41
41
  let monthDateFormatter = useDateFormatter({month: 'long', timeZone: options.timeZone});
42
42
  let hourDateFormatter = useDateFormatter({
@@ -45,14 +45,17 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
45
45
  timeZone: options.timeZone
46
46
  });
47
47
 
48
- if (segment.type === 'month') {
48
+ if (segment.type === 'month' && !segment.isPlaceholder) {
49
49
  let monthTextValue = monthDateFormatter.format(state.dateValue);
50
50
  textValue = monthTextValue !== textValue ? `${textValue} – ${monthTextValue}` : monthTextValue;
51
- } else if (segment.type === 'hour') {
51
+ } else if (segment.type === 'hour' && !segment.isPlaceholder) {
52
52
  textValue = hourDateFormatter.format(state.dateValue);
53
53
  }
54
54
 
55
55
  let {spinButtonProps} = useSpinButton({
56
+ // The ARIA spec says aria-valuenow is optional if there's no value, but aXe seems to require it.
57
+ // This doesn't seem to have any negative effects with real AT since we also use aria-valuetext.
58
+ // https://github.com/dequelabs/axe-core/issues/3505
56
59
  value: segment.value,
57
60
  textValue,
58
61
  minValue: segment.minValue,
@@ -115,34 +118,6 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
115
118
  }
116
119
 
117
120
  switch (e.key) {
118
- case 'ArrowLeft':
119
- e.preventDefault();
120
- e.stopPropagation();
121
- if (direction === 'rtl') {
122
- focusManager.focusNext({tabbable: true});
123
- } else {
124
- focusManager.focusPrevious({tabbable: true});
125
- }
126
- break;
127
- case 'ArrowRight':
128
- e.preventDefault();
129
- e.stopPropagation();
130
- if (direction === 'rtl') {
131
- focusManager.focusPrevious({tabbable: true});
132
- } else {
133
- focusManager.focusNext({tabbable: true});
134
- }
135
- break;
136
- case 'Enter':
137
- e.preventDefault();
138
- e.stopPropagation();
139
- if (segment.isPlaceholder && !state.isReadOnly) {
140
- state.confirmPlaceholder(segment.type);
141
- }
142
- focusManager.focusNext({tabbable: true});
143
- break;
144
- case 'Tab':
145
- break;
146
121
  case 'Backspace':
147
122
  case 'Delete': {
148
123
  // Safari on iOS does not fire beforeinput for the backspace key because the cursor is at the start.
@@ -169,6 +144,34 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
169
144
  return amPmFormatter.formatToParts(date).find(part => part.type === 'dayPeriod').value;
170
145
  }, [amPmFormatter]);
171
146
 
147
+ // Get a list of formatted era names so users can type the first character to choose one.
148
+ let eraFormatter = useDateFormatter({year: 'numeric', era: 'narrow', timeZone: 'UTC'});
149
+ let eras = useMemo(() => {
150
+ if (segment.type !== 'era') {
151
+ return [];
152
+ }
153
+
154
+ let date = toCalendar(new CalendarDate(1, 1, 1), state.calendar);
155
+ let eras = state.calendar.getEras().map(era => {
156
+ let eraDate = date.set({year: 1, month: 1, day: 1, era}).toDate('UTC');
157
+ let parts = eraFormatter.formatToParts(eraDate);
158
+ let formatted = parts.find(p => p.type === 'era').value;
159
+ return {era, formatted};
160
+ });
161
+
162
+ // Remove the common prefix from formatted values. This is so that in calendars with eras like
163
+ // ERA0 and ERA1 (e.g. Ethiopic), users can press "0" and "1" to select an era. In other cases,
164
+ // the first letter is used.
165
+ let prefixLength = commonPrefixLength(eras.map(era => era.formatted));
166
+ if (prefixLength) {
167
+ for (let era of eras) {
168
+ era.formatted = era.formatted.slice(prefixLength);
169
+ }
170
+ }
171
+
172
+ return eras;
173
+ }, [eraFormatter, state.calendar, segment.type]);
174
+
172
175
  let onInput = (key: string) => {
173
176
  if (state.isDisabled || state.isReadOnly) {
174
177
  return;
@@ -185,8 +188,16 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
185
188
  } else {
186
189
  break;
187
190
  }
188
- focusManager.focusNext({tabbable: true});
191
+ focusManager.focusNext();
192
+ break;
193
+ case 'era': {
194
+ let matched = eras.find(e => startsWith(e.formatted, key));
195
+ if (matched) {
196
+ state.setSegment('era', matched.era);
197
+ focusManager.focusNext();
198
+ }
189
199
  break;
200
+ }
190
201
  case 'day':
191
202
  case 'hour':
192
203
  case 'minute':
@@ -234,7 +245,7 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
234
245
  if (Number(numberValue + '0') > segment.maxValue || newValue.length >= String(segment.maxValue).length) {
235
246
  enteredKeys.current = '';
236
247
  if (shouldSetValue) {
237
- focusManager.focusNext({tabbable: true});
248
+ focusManager.focusNext();
238
249
  }
239
250
  } else {
240
251
  enteredKeys.current = newValue;
@@ -248,12 +259,9 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
248
259
  enteredKeys.current = '';
249
260
  scrollIntoView(getScrollParent(ref.current) as HTMLElement, ref.current);
250
261
 
251
- // Safari requires that a selection is set or it won't fire input events.
252
- // Since usePress disables text selection, this won't happen by default.
253
- ref.current.style.webkitUserSelect = 'text';
262
+ // Collapse selection to start or Chrome won't fire input events.
254
263
  let selection = window.getSelection();
255
264
  selection.collapse(ref.current);
256
- ref.current.style.webkitUserSelect = '';
257
265
  };
258
266
 
259
267
  let compositionRef = useRef('');
@@ -301,26 +309,18 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
301
309
  }
302
310
  });
303
311
 
304
- // Focus on mouse down/touch up to match native textfield behavior.
305
- // usePress handles canceling text selection.
306
- let {pressProps} = usePress({
307
- preventFocusOnPress: true,
308
- onPressStart: (e) => {
309
- if (e.pointerType === 'mouse') {
310
- e.target.focus();
311
- }
312
- },
313
- onPress(e) {
314
- if (e.pointerType !== 'mouse') {
315
- e.target.focus();
312
+ useLayoutEffect(() => {
313
+ let element = ref.current;
314
+ return () => {
315
+ // If the focused segment is removed, focus the previous one, or the next one if there was no previous one.
316
+ if (document.activeElement === element) {
317
+ let prev = focusManager.focusPrevious();
318
+ if (!prev) {
319
+ focusManager.focusNext();
320
+ }
316
321
  }
317
- }
318
- });
319
-
320
- // For Android: prevent selection on long press.
321
- useEvent(ref, 'selectstart', e => {
322
- e.preventDefault();
323
- });
322
+ };
323
+ }, [ref, focusManager]);
324
324
 
325
325
  // spinbuttons cannot be focused with VoiceOver on iOS.
326
326
  let touchPropOverrides = isIOS() || segment.type === 'timeZoneName' ? {
@@ -360,13 +360,13 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
360
360
  }
361
361
 
362
362
  return {
363
- segmentProps: mergeProps(spinButtonProps, pressProps, labelProps, {
363
+ segmentProps: mergeProps(spinButtonProps, labelProps, {
364
364
  id,
365
365
  ...touchPropOverrides,
366
366
  'aria-invalid': state.validationState === 'invalid' ? 'true' : undefined,
367
367
  'aria-describedby': ariaDescribedBy,
368
- 'aria-placeholder': segment.isPlaceholder ? segment.text : undefined,
369
368
  'aria-readonly': state.isReadOnly || !segment.isEditable ? 'true' : undefined,
369
+ 'data-placeholder': segment.isPlaceholder || undefined,
370
370
  contentEditable: isEditable,
371
371
  suppressContentEditableWarning: isEditable,
372
372
  spellCheck: isEditable ? 'false' : undefined,
@@ -374,13 +374,33 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
374
374
  autoCorrect: isEditable ? 'off' : undefined,
375
375
  // Capitalization was changed in React 17...
376
376
  [parseInt(React.version, 10) >= 17 ? 'enterKeyHint' : 'enterkeyhint']: isEditable ? 'next' : undefined,
377
- inputMode: state.isDisabled || segment.type === 'dayPeriod' || !isEditable ? undefined : 'numeric',
377
+ inputMode: state.isDisabled || segment.type === 'dayPeriod' || segment.type === 'era' || !isEditable ? undefined : 'numeric',
378
378
  tabIndex: state.isDisabled ? undefined : 0,
379
379
  onKeyDown,
380
380
  onFocus,
381
381
  style: {
382
382
  caretColor: 'transparent'
383
+ },
384
+ // Prevent pointer events from reaching useDatePickerGroup, and allow native browser behavior to focus the segment.
385
+ onPointerDown(e) {
386
+ e.stopPropagation();
387
+ },
388
+ onMouseDown(e) {
389
+ e.stopPropagation();
383
390
  }
384
391
  })
385
392
  };
386
393
  }
394
+
395
+ function commonPrefixLength(strings: string[]): number {
396
+ // Sort the strings, and compare the characters in the first and last to find the common prefix.
397
+ strings.sort();
398
+ let first = strings[0];
399
+ let last = strings[strings.length - 1];
400
+ for (let i = 0; i < first.length; i++) {
401
+ if (first[i] !== last[i]) {
402
+ return i;
403
+ }
404
+ }
405
+ return 0;
406
+ }
@@ -21,6 +21,7 @@ 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();
26
27
  return useMemo(() => {