@react-aria/datepicker 3.0.0-rc.1 → 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/dist/main.js +84 -35
- package/dist/main.js.map +1 -1
- package/dist/module.js +85 -36
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +16 -15
- package/src/useDatePickerGroup.ts +2 -2
- package/src/useDateRangePicker.ts +1 -7
- package/src/useDateSegment.ts +73 -29
|
@@ -23,7 +23,6 @@ import intlMessages from '../intl/*.json';
|
|
|
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
28
|
export interface DateRangePickerAria {
|
|
@@ -80,11 +79,6 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
|
|
|
80
79
|
let dialogId = useId();
|
|
81
80
|
|
|
82
81
|
let groupProps = useDatePickerGroup(state, ref);
|
|
83
|
-
let {focusWithinProps} = useFocusWithin({
|
|
84
|
-
onBlurWithin() {
|
|
85
|
-
state.confirmPlaceholder();
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
82
|
|
|
89
83
|
let ariaDescribedBy = [descProps['aria-describedby'], fieldProps['aria-describedby']].filter(Boolean).join(' ') || undefined;
|
|
90
84
|
let focusManager = useMemo(() => createFocusManager(ref, {
|
|
@@ -111,7 +105,7 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
|
|
|
111
105
|
let domProps = filterDOMProps(props);
|
|
112
106
|
|
|
113
107
|
return {
|
|
114
|
-
groupProps: mergeProps(domProps, groupProps, fieldProps, descProps,
|
|
108
|
+
groupProps: mergeProps(domProps, groupProps, fieldProps, descProps, {
|
|
115
109
|
role: 'group',
|
|
116
110
|
'aria-disabled': props.isDisabled || null,
|
|
117
111
|
'aria-describedby': ariaDescribedBy
|
package/src/useDateSegment.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
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';
|
|
@@ -35,7 +36,7 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
|
|
|
35
36
|
let displayNames = useDisplayNames();
|
|
36
37
|
let {ariaLabel, ariaLabelledBy, ariaDescribedBy, focusManager} = hookData.get(state);
|
|
37
38
|
|
|
38
|
-
let textValue = segment.text;
|
|
39
|
+
let textValue = segment.isPlaceholder ? '' : segment.text;
|
|
39
40
|
let options = useMemo(() => state.dateFormatter.resolvedOptions(), [state.dateFormatter]);
|
|
40
41
|
let monthDateFormatter = useDateFormatter({month: 'long', timeZone: options.timeZone});
|
|
41
42
|
let hourDateFormatter = useDateFormatter({
|
|
@@ -44,14 +45,17 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
|
|
|
44
45
|
timeZone: options.timeZone
|
|
45
46
|
});
|
|
46
47
|
|
|
47
|
-
if (segment.type === 'month') {
|
|
48
|
+
if (segment.type === 'month' && !segment.isPlaceholder) {
|
|
48
49
|
let monthTextValue = monthDateFormatter.format(state.dateValue);
|
|
49
50
|
textValue = monthTextValue !== textValue ? `${textValue} – ${monthTextValue}` : monthTextValue;
|
|
50
|
-
} else if (segment.type === 'hour') {
|
|
51
|
+
} else if (segment.type === 'hour' && !segment.isPlaceholder) {
|
|
51
52
|
textValue = hourDateFormatter.format(state.dateValue);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
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
|
|
55
59
|
value: segment.value,
|
|
56
60
|
textValue,
|
|
57
61
|
minValue: segment.minValue,
|
|
@@ -114,16 +118,6 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
|
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
switch (e.key) {
|
|
117
|
-
case 'Enter':
|
|
118
|
-
e.preventDefault();
|
|
119
|
-
e.stopPropagation();
|
|
120
|
-
if (segment.isPlaceholder && !state.isReadOnly) {
|
|
121
|
-
state.confirmPlaceholder(segment.type);
|
|
122
|
-
}
|
|
123
|
-
focusManager.focusNext();
|
|
124
|
-
break;
|
|
125
|
-
case 'Tab':
|
|
126
|
-
break;
|
|
127
121
|
case 'Backspace':
|
|
128
122
|
case 'Delete': {
|
|
129
123
|
// Safari on iOS does not fire beforeinput for the backspace key because the cursor is at the start.
|
|
@@ -150,6 +144,34 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
|
|
|
150
144
|
return amPmFormatter.formatToParts(date).find(part => part.type === 'dayPeriod').value;
|
|
151
145
|
}, [amPmFormatter]);
|
|
152
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
|
+
|
|
153
175
|
let onInput = (key: string) => {
|
|
154
176
|
if (state.isDisabled || state.isReadOnly) {
|
|
155
177
|
return;
|
|
@@ -168,6 +190,14 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
|
|
|
168
190
|
}
|
|
169
191
|
focusManager.focusNext();
|
|
170
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
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
171
201
|
case 'day':
|
|
172
202
|
case 'hour':
|
|
173
203
|
case 'minute':
|
|
@@ -229,14 +259,9 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
|
|
|
229
259
|
enteredKeys.current = '';
|
|
230
260
|
scrollIntoView(getScrollParent(ref.current) as HTMLElement, ref.current);
|
|
231
261
|
|
|
232
|
-
//
|
|
233
|
-
// Since usePress disables text selection, this won't happen by default.
|
|
234
|
-
ref.current.style.webkitUserSelect = 'text';
|
|
235
|
-
ref.current.style.userSelect = 'text';
|
|
262
|
+
// Collapse selection to start or Chrome won't fire input events.
|
|
236
263
|
let selection = window.getSelection();
|
|
237
264
|
selection.collapse(ref.current);
|
|
238
|
-
ref.current.style.webkitUserSelect = 'none';
|
|
239
|
-
ref.current.style.userSelect = 'none';
|
|
240
265
|
};
|
|
241
266
|
|
|
242
267
|
let compositionRef = useRef('');
|
|
@@ -284,10 +309,18 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
|
|
|
284
309
|
}
|
|
285
310
|
});
|
|
286
311
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
+
}
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}, [ref, focusManager]);
|
|
291
324
|
|
|
292
325
|
// spinbuttons cannot be focused with VoiceOver on iOS.
|
|
293
326
|
let touchPropOverrides = isIOS() || segment.type === 'timeZoneName' ? {
|
|
@@ -332,8 +365,8 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
|
|
|
332
365
|
...touchPropOverrides,
|
|
333
366
|
'aria-invalid': state.validationState === 'invalid' ? 'true' : undefined,
|
|
334
367
|
'aria-describedby': ariaDescribedBy,
|
|
335
|
-
'aria-placeholder': segment.isPlaceholder ? segment.text : undefined,
|
|
336
368
|
'aria-readonly': state.isReadOnly || !segment.isEditable ? 'true' : undefined,
|
|
369
|
+
'data-placeholder': segment.isPlaceholder || undefined,
|
|
337
370
|
contentEditable: isEditable,
|
|
338
371
|
suppressContentEditableWarning: isEditable,
|
|
339
372
|
spellCheck: isEditable ? 'false' : undefined,
|
|
@@ -341,14 +374,12 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
|
|
|
341
374
|
autoCorrect: isEditable ? 'off' : undefined,
|
|
342
375
|
// Capitalization was changed in React 17...
|
|
343
376
|
[parseInt(React.version, 10) >= 17 ? 'enterKeyHint' : 'enterkeyhint']: isEditable ? 'next' : undefined,
|
|
344
|
-
inputMode: state.isDisabled || segment.type === 'dayPeriod' || !isEditable ? undefined : 'numeric',
|
|
377
|
+
inputMode: state.isDisabled || segment.type === 'dayPeriod' || segment.type === 'era' || !isEditable ? undefined : 'numeric',
|
|
345
378
|
tabIndex: state.isDisabled ? undefined : 0,
|
|
346
379
|
onKeyDown,
|
|
347
380
|
onFocus,
|
|
348
381
|
style: {
|
|
349
|
-
caretColor: 'transparent'
|
|
350
|
-
userSelect: 'none',
|
|
351
|
-
WebkitUserSelect: 'none'
|
|
382
|
+
caretColor: 'transparent'
|
|
352
383
|
},
|
|
353
384
|
// Prevent pointer events from reaching useDatePickerGroup, and allow native browser behavior to focus the segment.
|
|
354
385
|
onPointerDown(e) {
|
|
@@ -360,3 +391,16 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
|
|
|
360
391
|
})
|
|
361
392
|
};
|
|
362
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
|
+
}
|