@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.
- package/dist/ar-AE.main.js +21 -0
- package/dist/ar-AE.main.js.map +1 -0
- package/dist/ar-AE.mjs +23 -0
- package/dist/ar-AE.module.js +23 -0
- package/dist/ar-AE.module.js.map +1 -0
- package/dist/bg-BG.main.js +21 -0
- package/dist/bg-BG.main.js.map +1 -0
- package/dist/bg-BG.mjs +23 -0
- package/dist/bg-BG.module.js +23 -0
- package/dist/bg-BG.module.js.map +1 -0
- package/dist/cs-CZ.main.js +21 -0
- package/dist/cs-CZ.main.js.map +1 -0
- package/dist/cs-CZ.mjs +23 -0
- package/dist/cs-CZ.module.js +23 -0
- package/dist/cs-CZ.module.js.map +1 -0
- package/dist/da-DK.main.js +21 -0
- package/dist/da-DK.main.js.map +1 -0
- package/dist/da-DK.mjs +23 -0
- package/dist/da-DK.module.js +23 -0
- package/dist/da-DK.module.js.map +1 -0
- package/dist/de-DE.main.js +21 -0
- package/dist/de-DE.main.js.map +1 -0
- package/dist/de-DE.mjs +23 -0
- package/dist/de-DE.module.js +23 -0
- package/dist/de-DE.module.js.map +1 -0
- package/dist/el-GR.main.js +21 -0
- package/dist/el-GR.main.js.map +1 -0
- package/dist/el-GR.mjs +23 -0
- package/dist/el-GR.module.js +23 -0
- package/dist/el-GR.module.js.map +1 -0
- package/dist/en-US.main.js +21 -0
- package/dist/en-US.main.js.map +1 -0
- package/dist/en-US.mjs +23 -0
- package/dist/en-US.module.js +23 -0
- package/dist/en-US.module.js.map +1 -0
- package/dist/es-ES.main.js +21 -0
- package/dist/es-ES.main.js.map +1 -0
- package/dist/es-ES.mjs +23 -0
- package/dist/es-ES.module.js +23 -0
- package/dist/es-ES.module.js.map +1 -0
- package/dist/et-EE.main.js +21 -0
- package/dist/et-EE.main.js.map +1 -0
- package/dist/et-EE.mjs +23 -0
- package/dist/et-EE.module.js +23 -0
- package/dist/et-EE.module.js.map +1 -0
- package/dist/fi-FI.main.js +21 -0
- package/dist/fi-FI.main.js.map +1 -0
- package/dist/fi-FI.mjs +23 -0
- package/dist/fi-FI.module.js +23 -0
- package/dist/fi-FI.module.js.map +1 -0
- package/dist/fr-FR.main.js +21 -0
- package/dist/fr-FR.main.js.map +1 -0
- package/dist/fr-FR.mjs +23 -0
- package/dist/fr-FR.module.js +23 -0
- package/dist/fr-FR.module.js.map +1 -0
- package/dist/he-IL.main.js +21 -0
- package/dist/he-IL.main.js.map +1 -0
- package/dist/he-IL.mjs +23 -0
- package/dist/he-IL.module.js +23 -0
- package/dist/he-IL.module.js.map +1 -0
- package/dist/hr-HR.main.js +21 -0
- package/dist/hr-HR.main.js.map +1 -0
- package/dist/hr-HR.mjs +23 -0
- package/dist/hr-HR.module.js +23 -0
- package/dist/hr-HR.module.js.map +1 -0
- package/dist/hu-HU.main.js +21 -0
- package/dist/hu-HU.main.js.map +1 -0
- package/dist/hu-HU.mjs +23 -0
- package/dist/hu-HU.module.js +23 -0
- package/dist/hu-HU.module.js.map +1 -0
- package/dist/import.mjs +25 -0
- package/dist/intlStrings.main.js +108 -0
- package/dist/intlStrings.main.js.map +1 -0
- package/dist/intlStrings.mjs +110 -0
- package/dist/intlStrings.module.js +110 -0
- package/dist/intlStrings.module.js.map +1 -0
- package/dist/it-IT.main.js +21 -0
- package/dist/it-IT.main.js.map +1 -0
- package/dist/it-IT.mjs +23 -0
- package/dist/it-IT.module.js +23 -0
- package/dist/it-IT.module.js.map +1 -0
- package/dist/ja-JP.main.js +21 -0
- package/dist/ja-JP.main.js.map +1 -0
- package/dist/ja-JP.mjs +23 -0
- package/dist/ja-JP.module.js +23 -0
- package/dist/ja-JP.module.js.map +1 -0
- package/dist/ko-KR.main.js +21 -0
- package/dist/ko-KR.main.js.map +1 -0
- package/dist/ko-KR.mjs +23 -0
- package/dist/ko-KR.module.js +23 -0
- package/dist/ko-KR.module.js.map +1 -0
- package/dist/lt-LT.main.js +21 -0
- package/dist/lt-LT.main.js.map +1 -0
- package/dist/lt-LT.mjs +23 -0
- package/dist/lt-LT.module.js +23 -0
- package/dist/lt-LT.module.js.map +1 -0
- package/dist/lv-LV.main.js +21 -0
- package/dist/lv-LV.main.js.map +1 -0
- package/dist/lv-LV.mjs +23 -0
- package/dist/lv-LV.module.js +23 -0
- package/dist/lv-LV.module.js.map +1 -0
- package/dist/main.js +22 -759
- package/dist/main.js.map +1 -1
- package/dist/module.js +17 -744
- package/dist/module.js.map +1 -1
- package/dist/nb-NO.main.js +21 -0
- package/dist/nb-NO.main.js.map +1 -0
- package/dist/nb-NO.mjs +23 -0
- package/dist/nb-NO.module.js +23 -0
- package/dist/nb-NO.module.js.map +1 -0
- package/dist/nl-NL.main.js +21 -0
- package/dist/nl-NL.main.js.map +1 -0
- package/dist/nl-NL.mjs +23 -0
- package/dist/nl-NL.module.js +23 -0
- package/dist/nl-NL.module.js.map +1 -0
- package/dist/pl-PL.main.js +21 -0
- package/dist/pl-PL.main.js.map +1 -0
- package/dist/pl-PL.mjs +23 -0
- package/dist/pl-PL.module.js +23 -0
- package/dist/pl-PL.module.js.map +1 -0
- package/dist/pt-BR.main.js +21 -0
- package/dist/pt-BR.main.js.map +1 -0
- package/dist/pt-BR.mjs +23 -0
- package/dist/pt-BR.module.js +23 -0
- package/dist/pt-BR.module.js.map +1 -0
- package/dist/pt-PT.main.js +21 -0
- package/dist/pt-PT.main.js.map +1 -0
- package/dist/pt-PT.mjs +23 -0
- package/dist/pt-PT.module.js +23 -0
- package/dist/pt-PT.module.js.map +1 -0
- package/dist/ro-RO.main.js +21 -0
- package/dist/ro-RO.main.js.map +1 -0
- package/dist/ro-RO.mjs +23 -0
- package/dist/ro-RO.module.js +23 -0
- package/dist/ro-RO.module.js.map +1 -0
- package/dist/ru-RU.main.js +21 -0
- package/dist/ru-RU.main.js.map +1 -0
- package/dist/ru-RU.mjs +23 -0
- package/dist/ru-RU.module.js +23 -0
- package/dist/ru-RU.module.js.map +1 -0
- package/dist/sk-SK.main.js +21 -0
- package/dist/sk-SK.main.js.map +1 -0
- package/dist/sk-SK.mjs +23 -0
- package/dist/sk-SK.module.js +23 -0
- package/dist/sk-SK.module.js.map +1 -0
- package/dist/sl-SI.main.js +21 -0
- package/dist/sl-SI.main.js.map +1 -0
- package/dist/sl-SI.mjs +23 -0
- package/dist/sl-SI.module.js +23 -0
- package/dist/sl-SI.module.js.map +1 -0
- package/dist/sr-SP.main.js +21 -0
- package/dist/sr-SP.main.js.map +1 -0
- package/dist/sr-SP.mjs +23 -0
- package/dist/sr-SP.module.js +23 -0
- package/dist/sr-SP.module.js.map +1 -0
- package/dist/sv-SE.main.js +21 -0
- package/dist/sv-SE.main.js.map +1 -0
- package/dist/sv-SE.mjs +23 -0
- package/dist/sv-SE.module.js +23 -0
- package/dist/sv-SE.module.js.map +1 -0
- package/dist/tr-TR.main.js +21 -0
- package/dist/tr-TR.main.js.map +1 -0
- package/dist/tr-TR.mjs +23 -0
- package/dist/tr-TR.module.js +23 -0
- package/dist/tr-TR.module.js.map +1 -0
- package/dist/types.d.ts +88 -37
- package/dist/types.d.ts.map +1 -1
- package/dist/uk-UA.main.js +21 -0
- package/dist/uk-UA.main.js.map +1 -0
- package/dist/uk-UA.mjs +23 -0
- package/dist/uk-UA.module.js +23 -0
- package/dist/uk-UA.module.js.map +1 -0
- package/dist/useDateField.main.js +177 -0
- package/dist/useDateField.main.js.map +1 -0
- package/dist/useDateField.mjs +168 -0
- package/dist/useDateField.module.js +168 -0
- package/dist/useDateField.module.js.map +1 -0
- package/dist/useDatePicker.main.js +158 -0
- package/dist/useDatePicker.main.js.map +1 -0
- package/dist/useDatePicker.mjs +153 -0
- package/dist/useDatePicker.module.js +153 -0
- package/dist/useDatePicker.module.js.map +1 -0
- package/dist/useDatePickerGroup.main.js +91 -0
- package/dist/useDatePickerGroup.main.js.map +1 -0
- package/dist/useDatePickerGroup.mjs +86 -0
- package/dist/useDatePickerGroup.module.js +86 -0
- package/dist/useDatePickerGroup.module.js.map +1 -0
- package/dist/useDateRangePicker.main.js +201 -0
- package/dist/useDateRangePicker.main.js.map +1 -0
- package/dist/useDateRangePicker.mjs +196 -0
- package/dist/useDateRangePicker.module.js +196 -0
- package/dist/useDateRangePicker.module.js.map +1 -0
- package/dist/useDateSegment.main.js +373 -0
- package/dist/useDateSegment.main.js.map +1 -0
- package/dist/useDateSegment.mjs +364 -0
- package/dist/useDateSegment.module.js +364 -0
- package/dist/useDateSegment.module.js.map +1 -0
- package/dist/useDisplayNames.main.js +59 -0
- package/dist/useDisplayNames.main.js.map +1 -0
- package/dist/useDisplayNames.mjs +54 -0
- package/dist/useDisplayNames.module.js +54 -0
- package/dist/useDisplayNames.module.js.map +1 -0
- package/dist/zh-CN.main.js +21 -0
- package/dist/zh-CN.main.js.map +1 -0
- package/dist/zh-CN.mjs +23 -0
- package/dist/zh-CN.module.js +23 -0
- package/dist/zh-CN.module.js.map +1 -0
- package/dist/zh-TW.main.js +21 -0
- package/dist/zh-TW.main.js.map +1 -0
- package/dist/zh-TW.mjs +23 -0
- package/dist/zh-TW.module.js +23 -0
- package/dist/zh-TW.module.js.map +1 -0
- package/package.json +27 -18
- package/src/index.ts +12 -5
- package/src/useDateField.ts +163 -32
- package/src/useDatePicker.ts +113 -26
- package/src/useDatePickerGroup.ts +71 -13
- package/src/useDateRangePicker.ts +149 -38
- package/src/useDateSegment.ts +145 -91
- package/src/useDisplayNames.ts +10 -8
package/src/useDateSegment.ts
CHANGED
|
@@ -10,30 +10,34 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
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, {
|
|
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
|
-
|
|
24
|
+
export interface DateSegmentAria {
|
|
25
|
+
/** Props for the segment element. */
|
|
26
|
+
segmentProps: React.HTMLAttributes<HTMLDivElement>
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
|
|
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
|
|
36
|
+
let {locale} = useLocale();
|
|
33
37
|
let displayNames = useDisplayNames();
|
|
34
|
-
let focusManager =
|
|
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'
|
|
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:
|
|
58
|
-
isReadOnly:
|
|
59
|
-
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 (
|
|
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 (
|
|
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
|
-
|
|
265
|
+
scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
|
|
247
266
|
|
|
248
|
-
//
|
|
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) && !
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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.
|
|
353
|
+
if (segment !== firstSegment && !state.isInvalid) {
|
|
337
354
|
ariaDescribedBy = undefined;
|
|
338
355
|
}
|
|
339
356
|
|
|
340
|
-
let id = useId(
|
|
341
|
-
let 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,
|
|
379
|
+
segmentProps: mergeProps(spinButtonProps, labelProps, {
|
|
344
380
|
id,
|
|
345
381
|
...touchPropOverrides,
|
|
346
|
-
'aria-
|
|
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-
|
|
353
|
-
'
|
|
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:
|
|
362
|
-
tabIndex:
|
|
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
|
+
}
|
package/src/useDisplayNames.ts
CHANGED
|
@@ -12,17 +12,19 @@
|
|
|
12
12
|
|
|
13
13
|
// @ts-ignore
|
|
14
14
|
import intlMessages from '../intl/*.json';
|
|
15
|
-
import {
|
|
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 =
|
|
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:
|
|
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 =
|
|
46
|
+
this.dictionary = dictionary;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
of(field: Field): string {
|