@react-aria/datepicker 3.0.0-nightly.3132 → 3.0.0-nightly.3148
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 +246 -135
- package/dist/main.js.map +1 -1
- package/dist/module.js +241 -132
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +66 -27
- package/dist/types.d.ts.map +1 -1
- package/package.json +16 -15
- package/src/index.ts +4 -4
- package/src/useDateField.ts +43 -8
- package/src/useDatePicker.ts +47 -9
- package/src/useDatePickerGroup.ts +31 -8
- package/src/useDateRangePicker.ts +62 -13
- package/src/useDateSegment.ts +44 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-aria/datepicker",
|
|
3
|
-
"version": "3.0.0-nightly.
|
|
3
|
+
"version": "3.0.0-nightly.3148+320c69908",
|
|
4
4
|
"description": "Spectrum UI components in React",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -18,19 +18,20 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@babel/runtime": "^7.6.2",
|
|
21
|
-
"@internationalized/message": "3.0.6-nightly.
|
|
22
|
-
"@internationalized/number": "3.0.6-nightly.
|
|
23
|
-
"@react-aria/focus": "3.0.0-nightly.
|
|
24
|
-
"@react-aria/i18n": "3.0.0-nightly.
|
|
25
|
-
"@react-aria/interactions": "3.0.0-nightly.
|
|
26
|
-
"@react-aria/label": "3.0.0-nightly.
|
|
27
|
-
"@react-aria/spinbutton": "3.0.0-nightly.
|
|
28
|
-
"@react-aria/utils": "3.0.0-nightly.
|
|
29
|
-
"@react-stately/datepicker": "3.0.0-nightly.
|
|
30
|
-
"@react-types/button": "3.4.4-nightly.
|
|
31
|
-
"@react-types/
|
|
32
|
-
"@react-types/
|
|
33
|
-
"@react-types/
|
|
21
|
+
"@internationalized/message": "3.0.6-nightly.3148+320c69908",
|
|
22
|
+
"@internationalized/number": "3.0.6-nightly.3148+320c69908",
|
|
23
|
+
"@react-aria/focus": "3.0.0-nightly.1452+320c69908",
|
|
24
|
+
"@react-aria/i18n": "3.0.0-nightly.1452+320c69908",
|
|
25
|
+
"@react-aria/interactions": "3.0.0-nightly.1452+320c69908",
|
|
26
|
+
"@react-aria/label": "3.0.0-nightly.1452+320c69908",
|
|
27
|
+
"@react-aria/spinbutton": "3.0.0-nightly.1452+320c69908",
|
|
28
|
+
"@react-aria/utils": "3.0.0-nightly.1452+320c69908",
|
|
29
|
+
"@react-stately/datepicker": "3.0.0-nightly.3148+320c69908",
|
|
30
|
+
"@react-types/button": "3.4.4-nightly.3148+320c69908",
|
|
31
|
+
"@react-types/calendar": "3.0.0-nightly.3148+320c69908",
|
|
32
|
+
"@react-types/datepicker": "3.0.0-nightly.3148+320c69908",
|
|
33
|
+
"@react-types/dialog": "3.3.4-nightly.3148+320c69908",
|
|
34
|
+
"@react-types/shared": "3.0.0-nightly.1452+320c69908"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
37
|
"react": "^16.8.0 || ^17.0.0-rc.1",
|
|
@@ -39,5 +40,5 @@
|
|
|
39
40
|
"publishConfig": {
|
|
40
41
|
"access": "public"
|
|
41
42
|
},
|
|
42
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "320c69908b41be47a3abb2878d04f90384e54321"
|
|
43
44
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
export
|
|
14
|
-
export
|
|
15
|
-
export
|
|
16
|
-
export
|
|
13
|
+
export {useDatePicker} from './useDatePicker';
|
|
14
|
+
export {useDateSegment} from './useDateSegment';
|
|
15
|
+
export {useDateField, useTimeField} from './useDateField';
|
|
16
|
+
export {useDateRangePicker} from './useDateRangePicker';
|
|
17
17
|
export * from './useDisplayNames';
|
package/src/useDateField.ts
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {AriaDatePickerProps, DateValue} from '@react-types/datepicker';
|
|
14
|
-
import {createFocusManager} from '@react-aria/focus';
|
|
13
|
+
import {AriaDatePickerProps, AriaTimeFieldProps, DateValue, TimeValue} from '@react-types/datepicker';
|
|
14
|
+
import {createFocusManager, FocusManager} from '@react-aria/focus';
|
|
15
15
|
import {DatePickerFieldState} from '@react-stately/datepicker';
|
|
16
|
-
import {
|
|
16
|
+
import {focusManagerSymbol} from './useDateRangePicker';
|
|
17
|
+
import {HTMLAttributes, RefObject, useEffect, useMemo, useRef} from 'react';
|
|
17
18
|
import {mergeProps, useDescription} from '@react-aria/utils';
|
|
18
19
|
import {useDateFormatter} from '@react-aria/i18n';
|
|
19
20
|
import {useDatePickerGroup} from './useDatePickerGroup';
|
|
@@ -24,7 +25,9 @@ import {useFocusWithin} from '@react-aria/interactions';
|
|
|
24
25
|
interface DateFieldProps<T extends DateValue> extends Omit<AriaDatePickerProps<T>, 'value' | 'defaultValue' | 'onChange' | 'minValue' | 'maxValue' | 'placeholderValue'> {}
|
|
25
26
|
|
|
26
27
|
interface DateFieldAria {
|
|
27
|
-
|
|
28
|
+
/** Props for the field's visible label element, if any. */
|
|
29
|
+
labelProps: HTMLAttributes<HTMLElement>,
|
|
30
|
+
/** Props for the field grouping element. */
|
|
28
31
|
fieldProps: HTMLAttributes<HTMLElement>,
|
|
29
32
|
/** Props for the description element, if any. */
|
|
30
33
|
descriptionProps: HTMLAttributes<HTMLElement>,
|
|
@@ -32,8 +35,20 @@ interface DateFieldAria {
|
|
|
32
35
|
errorMessageProps: HTMLAttributes<HTMLElement>
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
// Data that is passed between useDateField and useDateSegment.
|
|
39
|
+
interface HookData {
|
|
40
|
+
ariaLabelledBy: string,
|
|
41
|
+
ariaDescribedBy: string,
|
|
42
|
+
focusManager: FocusManager
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const hookData = new WeakMap<DatePickerFieldState, HookData>();
|
|
36
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Provides the behavior and accessibility implementation for a date field component.
|
|
49
|
+
* A date field allows users to enter and edit date and time values using a keyboard.
|
|
50
|
+
* Each part of a date value is displayed in an individually editable segment.
|
|
51
|
+
*/
|
|
37
52
|
export function useDateField<T extends DateValue>(props: DateFieldProps<T>, state: DatePickerFieldState, ref: RefObject<HTMLElement>): DateFieldAria {
|
|
38
53
|
let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({
|
|
39
54
|
...props,
|
|
@@ -53,17 +68,28 @@ export function useDateField<T extends DateValue>(props: DateFieldProps<T>, stat
|
|
|
53
68
|
|
|
54
69
|
let segmentLabelledBy = fieldProps['aria-labelledby'] || fieldProps.id;
|
|
55
70
|
let describedBy = [descProps['aria-describedby'], fieldProps['aria-describedby']].filter(Boolean).join(' ') || undefined;
|
|
71
|
+
let propsFocusManager = props[focusManagerSymbol];
|
|
72
|
+
let focusManager = useMemo(() => propsFocusManager || createFocusManager(ref), [propsFocusManager, ref]);
|
|
56
73
|
|
|
57
|
-
|
|
74
|
+
hookData.set(state, {
|
|
58
75
|
ariaLabelledBy: segmentLabelledBy,
|
|
59
|
-
ariaDescribedBy: describedBy
|
|
76
|
+
ariaDescribedBy: describedBy,
|
|
77
|
+
focusManager
|
|
60
78
|
});
|
|
61
79
|
|
|
80
|
+
let autoFocusRef = useRef(props.autoFocus);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (autoFocusRef.current) {
|
|
84
|
+
focusManager.focusFirst();
|
|
85
|
+
}
|
|
86
|
+
autoFocusRef.current = false;
|
|
87
|
+
}, [focusManager]);
|
|
88
|
+
|
|
62
89
|
return {
|
|
63
90
|
labelProps: {
|
|
64
91
|
...labelProps,
|
|
65
92
|
onClick: () => {
|
|
66
|
-
let focusManager = createFocusManager(ref);
|
|
67
93
|
focusManager.focusFirst();
|
|
68
94
|
}
|
|
69
95
|
},
|
|
@@ -76,3 +102,12 @@ export function useDateField<T extends DateValue>(props: DateFieldProps<T>, stat
|
|
|
76
102
|
errorMessageProps
|
|
77
103
|
};
|
|
78
104
|
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Provides the behavior and accessibility implementation for a time field component.
|
|
108
|
+
* A time field allows users to enter and edit time values using a keyboard.
|
|
109
|
+
* Each part of a time value is displayed in an individually editable segment.
|
|
110
|
+
*/
|
|
111
|
+
export function useTimeField<T extends TimeValue>(props: AriaTimeFieldProps<T>, state: DatePickerFieldState, ref: RefObject<HTMLElement>): DateFieldAria {
|
|
112
|
+
return useDateField(props, state, ref);
|
|
113
|
+
}
|
package/src/useDatePicker.ts
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
import {AriaButtonProps} from '@react-types/button';
|
|
14
14
|
import {AriaDatePickerProps, DateValue} from '@react-types/datepicker';
|
|
15
15
|
import {AriaDialogProps} from '@react-types/dialog';
|
|
16
|
+
import {CalendarProps} from '@react-types/calendar';
|
|
16
17
|
import {createFocusManager} from '@react-aria/focus';
|
|
17
18
|
import {DatePickerState} from '@react-stately/datepicker';
|
|
18
|
-
import {HTMLAttributes,
|
|
19
|
+
import {HTMLAttributes, RefObject} from 'react';
|
|
19
20
|
// @ts-ignore
|
|
20
21
|
import intlMessages from '../intl/*.json';
|
|
21
22
|
import {mergeProps, useDescription, useId} from '@react-aria/utils';
|
|
@@ -23,19 +24,30 @@ import {useDatePickerGroup} from './useDatePickerGroup';
|
|
|
23
24
|
import {useField} from '@react-aria/label';
|
|
24
25
|
import {useLocale, useMessageFormatter} from '@react-aria/i18n';
|
|
25
26
|
|
|
26
|
-
interface DatePickerAria
|
|
27
|
+
interface DatePickerAria {
|
|
28
|
+
/** Props for the date picker's visible label element, if any. */
|
|
29
|
+
labelProps: HTMLAttributes<HTMLElement>,
|
|
30
|
+
/** Props for the grouping element containing the date field and button. */
|
|
27
31
|
groupProps: HTMLAttributes<HTMLElement>,
|
|
28
|
-
|
|
29
|
-
fieldProps: AriaDatePickerProps<
|
|
32
|
+
/** Props for the date field. */
|
|
33
|
+
fieldProps: AriaDatePickerProps<DateValue>,
|
|
34
|
+
/** Props for the popover trigger button. */
|
|
35
|
+
buttonProps: AriaButtonProps,
|
|
30
36
|
/** Props for the description element, if any. */
|
|
31
37
|
descriptionProps: HTMLAttributes<HTMLElement>,
|
|
32
38
|
/** Props for the error message element, if any. */
|
|
33
39
|
errorMessageProps: HTMLAttributes<HTMLElement>,
|
|
34
|
-
|
|
35
|
-
dialogProps: AriaDialogProps
|
|
40
|
+
/** Props for the popover dialog. */
|
|
41
|
+
dialogProps: AriaDialogProps,
|
|
42
|
+
/** Props for the calendar within the popover dialog. */
|
|
43
|
+
calendarProps: CalendarProps<DateValue>
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Provides the behavior and accessibility implementation for a date picker component.
|
|
48
|
+
* A date picker combines a DateField and a Calendar popover to allow users to enter or select a date and time value.
|
|
49
|
+
*/
|
|
50
|
+
export function useDatePicker<T extends DateValue>(props: AriaDatePickerProps<T>, state: DatePickerState, ref: RefObject<HTMLElement>): DatePickerAria {
|
|
39
51
|
let buttonId = useId();
|
|
40
52
|
let dialogId = useId();
|
|
41
53
|
let formatMessage = useMessageFormatter(intlMessages);
|
|
@@ -67,7 +79,22 @@ export function useDatePicker<T extends DateValue>(props: AriaDatePickerProps<T>
|
|
|
67
79
|
focusManager.focusFirst();
|
|
68
80
|
}
|
|
69
81
|
},
|
|
70
|
-
fieldProps
|
|
82
|
+
fieldProps: {
|
|
83
|
+
...fieldProps,
|
|
84
|
+
value: state.value,
|
|
85
|
+
onChange: state.setValue,
|
|
86
|
+
minValue: props.minValue,
|
|
87
|
+
maxValue: props.maxValue,
|
|
88
|
+
placeholderValue: props.placeholderValue,
|
|
89
|
+
hideTimeZone: props.hideTimeZone,
|
|
90
|
+
hourCycle: props.hourCycle,
|
|
91
|
+
granularity: props.granularity,
|
|
92
|
+
isDisabled: props.isDisabled,
|
|
93
|
+
isReadOnly: props.isReadOnly,
|
|
94
|
+
isRequired: props.isRequired,
|
|
95
|
+
validationState: state.validationState,
|
|
96
|
+
autoFocus: props.autoFocus
|
|
97
|
+
},
|
|
71
98
|
descriptionProps,
|
|
72
99
|
errorMessageProps,
|
|
73
100
|
buttonProps: {
|
|
@@ -77,11 +104,22 @@ export function useDatePicker<T extends DateValue>(props: AriaDatePickerProps<T>
|
|
|
77
104
|
'aria-haspopup': 'dialog',
|
|
78
105
|
'aria-label': formatMessage('calendar'),
|
|
79
106
|
'aria-labelledby': `${labelledBy} ${buttonId}`,
|
|
80
|
-
'aria-describedby': ariaDescribedBy
|
|
107
|
+
'aria-describedby': ariaDescribedBy,
|
|
108
|
+
onPress: () => state.setOpen(true)
|
|
81
109
|
},
|
|
82
110
|
dialogProps: {
|
|
83
111
|
id: dialogId,
|
|
84
112
|
'aria-labelledby': `${labelledBy} ${buttonId}`
|
|
113
|
+
},
|
|
114
|
+
calendarProps: {
|
|
115
|
+
autoFocus: true,
|
|
116
|
+
value: state.dateValue,
|
|
117
|
+
onChange: state.setDateValue,
|
|
118
|
+
minValue: props.minValue,
|
|
119
|
+
maxValue: props.maxValue,
|
|
120
|
+
isDisabled: props.isDisabled,
|
|
121
|
+
isReadOnly: props.isReadOnly,
|
|
122
|
+
isDateDisabled: props.isDateDisabled
|
|
85
123
|
}
|
|
86
124
|
};
|
|
87
125
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {DatePickerFieldState, DatePickerState, DateRangePickerState} from '@react-stately/datepicker';
|
|
2
|
+
import {getFocusableTreeWalker} from '@react-aria/focus';
|
|
2
3
|
import {KeyboardEvent} from '@react-types/shared';
|
|
3
4
|
import {mergeProps} from '@react-aria/utils';
|
|
4
5
|
import {RefObject} from 'react';
|
|
@@ -16,15 +17,37 @@ export function useDatePickerGroup(state: DatePickerState | DateRangePickerState
|
|
|
16
17
|
|
|
17
18
|
// Focus the first placeholder segment from the end on mouse down/touch up in the field.
|
|
18
19
|
let focusLast = () => {
|
|
19
|
-
|
|
20
|
-
let
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
// Try to find the segment prior to the element that was clicked on.
|
|
21
|
+
let target = window.event?.target as HTMLElement;
|
|
22
|
+
let walker = getFocusableTreeWalker(ref.current, {tabbable: true});
|
|
23
|
+
if (target) {
|
|
24
|
+
walker.currentNode = target;
|
|
25
|
+
target = walker.previousNode() as HTMLElement;
|
|
23
26
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
// If no target found, find the last element from the end.
|
|
29
|
+
if (!target) {
|
|
30
|
+
let last: HTMLElement;
|
|
31
|
+
do {
|
|
32
|
+
last = walker.lastChild() as HTMLElement;
|
|
33
|
+
if (last) {
|
|
34
|
+
target = last;
|
|
35
|
+
}
|
|
36
|
+
} while (last);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Now go backwards until we find an element that is not a placeholder.
|
|
40
|
+
while (target?.getAttribute('aria-placeholder')) {
|
|
41
|
+
let prev = walker.previousNode() as HTMLElement;
|
|
42
|
+
if (prev && prev.getAttribute('aria-placeholder')) {
|
|
43
|
+
target = prev;
|
|
44
|
+
} else {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (target) {
|
|
50
|
+
target.focus();
|
|
28
51
|
}
|
|
29
52
|
};
|
|
30
53
|
|
|
@@ -15,29 +15,47 @@ 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 {HTMLAttributes,
|
|
18
|
+
import {HTMLAttributes, RefObject, useMemo} from 'react';
|
|
19
19
|
// @ts-ignore
|
|
20
20
|
import intlMessages from '../intl/*.json';
|
|
21
21
|
import {mergeProps, useDescription, useId, useLabels} from '@react-aria/utils';
|
|
22
|
+
import {RangeCalendarProps} from '@react-types/calendar';
|
|
22
23
|
import {useDatePickerGroup} from './useDatePickerGroup';
|
|
23
24
|
import {useField} from '@react-aria/label';
|
|
24
25
|
import {useFocusWithin} from '@react-aria/interactions';
|
|
25
26
|
import {useLocale, useMessageFormatter} from '@react-aria/i18n';
|
|
26
27
|
|
|
27
|
-
interface DateRangePickerAria
|
|
28
|
-
|
|
28
|
+
interface DateRangePickerAria {
|
|
29
|
+
/** Props for the date range picker's visible label element, if any. */
|
|
30
|
+
labelProps: HTMLAttributes<HTMLElement>,
|
|
31
|
+
/** Props for the grouping element containing the date fields and button. */
|
|
29
32
|
groupProps: HTMLAttributes<HTMLElement>,
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
/** Props for the start date field. */
|
|
34
|
+
startFieldProps: AriaDatePickerProps<DateValue>,
|
|
35
|
+
/** Props for the end date field. */
|
|
36
|
+
endFieldProps: AriaDatePickerProps<DateValue>,
|
|
37
|
+
/** Props for the popover trigger button. */
|
|
38
|
+
buttonProps: AriaButtonProps,
|
|
32
39
|
/** Props for the description element, if any. */
|
|
33
40
|
descriptionProps: HTMLAttributes<HTMLElement>,
|
|
34
41
|
/** Props for the error message element, if any. */
|
|
35
42
|
errorMessageProps: HTMLAttributes<HTMLElement>,
|
|
36
|
-
|
|
37
|
-
dialogProps:
|
|
43
|
+
/** Props for the popover dialog. */
|
|
44
|
+
dialogProps: AriaDialogProps,
|
|
45
|
+
/** Props for the range calendar within the popover dialog. */
|
|
46
|
+
calendarProps: RangeCalendarProps<DateValue>
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
|
|
49
|
+
// Used to pass the focus manager to the date fields.
|
|
50
|
+
// Ideally we'd use a Symbol for this, but React doesn't support them: https://github.com/facebook/react/issues/7552
|
|
51
|
+
export const focusManagerSymbol = '__focusManager_' + Date.now();
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Provides the behavior and accessibility implementation for a date picker component.
|
|
55
|
+
* A date range picker combines two DateFields and a RangeCalendar popover to allow
|
|
56
|
+
* users to enter or select a date and time range.
|
|
57
|
+
*/
|
|
58
|
+
export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePickerProps<T>, state: DateRangePickerState, ref: RefObject<HTMLElement>): DateRangePickerAria {
|
|
41
59
|
let formatMessage = useMessageFormatter(intlMessages);
|
|
42
60
|
let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({
|
|
43
61
|
...props,
|
|
@@ -71,6 +89,20 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
|
|
|
71
89
|
});
|
|
72
90
|
|
|
73
91
|
let ariaDescribedBy = [descProps['aria-describedby'], fieldProps['aria-describedby']].filter(Boolean).join(' ') || undefined;
|
|
92
|
+
let focusManager = useMemo(() => createFocusManager(ref), [ref]);
|
|
93
|
+
let commonFieldProps = {
|
|
94
|
+
[focusManagerSymbol]: focusManager,
|
|
95
|
+
minValue: props.minValue,
|
|
96
|
+
maxValue: props.maxValue,
|
|
97
|
+
placeholderValue: props.placeholderValue,
|
|
98
|
+
hideTimeZone: props.hideTimeZone,
|
|
99
|
+
hourCycle: props.hourCycle,
|
|
100
|
+
granularity: props.granularity,
|
|
101
|
+
isDisabled: props.isDisabled,
|
|
102
|
+
isReadOnly: props.isReadOnly,
|
|
103
|
+
isRequired: props.isRequired,
|
|
104
|
+
validationState: state.validationState
|
|
105
|
+
};
|
|
74
106
|
|
|
75
107
|
return {
|
|
76
108
|
groupProps: mergeProps(groupProps, fieldProps, descProps, focusWithinProps, {
|
|
@@ -81,7 +113,6 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
|
|
|
81
113
|
labelProps: {
|
|
82
114
|
...labelProps,
|
|
83
115
|
onClick: () => {
|
|
84
|
-
let focusManager = createFocusManager(ref);
|
|
85
116
|
focusManager.focusFirst();
|
|
86
117
|
}
|
|
87
118
|
},
|
|
@@ -92,7 +123,8 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
|
|
|
92
123
|
'aria-haspopup': 'dialog',
|
|
93
124
|
'aria-label': formatMessage('calendar'),
|
|
94
125
|
'aria-labelledby': `${labelledBy} ${buttonId}`,
|
|
95
|
-
'aria-describedby': ariaDescribedBy
|
|
126
|
+
'aria-describedby': ariaDescribedBy,
|
|
127
|
+
onPress: () => state.setOpen(true)
|
|
96
128
|
},
|
|
97
129
|
dialogProps: {
|
|
98
130
|
id: dialogId,
|
|
@@ -100,13 +132,30 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
|
|
|
100
132
|
},
|
|
101
133
|
startFieldProps: {
|
|
102
134
|
...startFieldProps,
|
|
103
|
-
|
|
135
|
+
...commonFieldProps,
|
|
136
|
+
'aria-describedby': fieldProps['aria-describedby'],
|
|
137
|
+
value: state.value?.start,
|
|
138
|
+
onChange: start => state.setDateTime('start', start),
|
|
139
|
+
autoFocus: props.autoFocus
|
|
104
140
|
},
|
|
105
141
|
endFieldProps: {
|
|
106
142
|
...endFieldProps,
|
|
107
|
-
|
|
143
|
+
...commonFieldProps,
|
|
144
|
+
'aria-describedby': fieldProps['aria-describedby'],
|
|
145
|
+
value: state.value?.end,
|
|
146
|
+
onChange: end => state.setDateTime('end', end)
|
|
108
147
|
},
|
|
109
148
|
descriptionProps,
|
|
110
|
-
errorMessageProps
|
|
149
|
+
errorMessageProps,
|
|
150
|
+
calendarProps: {
|
|
151
|
+
autoFocus: true,
|
|
152
|
+
value: state.dateRange,
|
|
153
|
+
onChange: state.setDateRange,
|
|
154
|
+
minValue: props.minValue,
|
|
155
|
+
maxValue: props.maxValue,
|
|
156
|
+
isDisabled: props.isDisabled,
|
|
157
|
+
isReadOnly: props.isReadOnly,
|
|
158
|
+
isDateDisabled: props.isDateDisabled
|
|
159
|
+
}
|
|
111
160
|
};
|
|
112
161
|
}
|
package/src/useDateSegment.ts
CHANGED
|
@@ -11,27 +11,30 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {DatePickerFieldState, DateSegment} from '@react-stately/datepicker';
|
|
14
|
-
import {DatePickerProps, DateValue} from '@react-types/datepicker';
|
|
15
|
-
import {DOMProps} from '@react-types/shared';
|
|
16
14
|
import {getScrollParent, isIOS, isMac, mergeProps, scrollIntoView, useEvent, useId} from '@react-aria/utils';
|
|
17
|
-
import {
|
|
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
|
-
|
|
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: DatePickerFieldState, ref: RefObject<HTMLElement>): DateSegmentAria {
|
|
31
34
|
let enteredKeys = useRef('');
|
|
32
35
|
let {locale, direction} = useLocale();
|
|
33
36
|
let displayNames = useDisplayNames();
|
|
34
|
-
let focusManager =
|
|
37
|
+
let {ariaLabelledBy, ariaDescribedBy, focusManager} = hookData.get(state);
|
|
35
38
|
|
|
36
39
|
let textValue = segment.text;
|
|
37
40
|
let options = useMemo(() => state.dateFormatter.resolvedOptions(), [state.dateFormatter]);
|
|
@@ -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:
|
|
58
|
-
isReadOnly:
|
|
59
|
-
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) && !
|
|
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 && !
|
|
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 (
|
|
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) && !
|
|
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,29 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
|
|
|
337
338
|
ariaDescribedBy = undefined;
|
|
338
339
|
}
|
|
339
340
|
|
|
340
|
-
let id = useId(
|
|
341
|
-
let isEditable = !
|
|
341
|
+
let id = useId();
|
|
342
|
+
let isEditable = !state.isDisabled && !state.isReadOnly && segment.isEditable;
|
|
343
|
+
|
|
344
|
+
// Literal segments should not be visible to screen readers. We don't really need any of the above,
|
|
345
|
+
// but the rules of hooks mean hooks cannot be conditional so we have to put this condition here.
|
|
346
|
+
if (segment.type === 'literal') {
|
|
347
|
+
return {
|
|
348
|
+
segmentProps: {
|
|
349
|
+
'aria-hidden': true
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
342
354
|
return {
|
|
343
355
|
segmentProps: mergeProps(spinButtonProps, pressProps, {
|
|
344
356
|
id,
|
|
345
357
|
...touchPropOverrides,
|
|
346
|
-
'aria-controls': props['aria-controls'],
|
|
347
|
-
// 'aria-haspopup': props['aria-haspopup'], // deprecated in ARIA 1.2
|
|
348
358
|
'aria-invalid': state.validationState === 'invalid' ? 'true' : undefined,
|
|
349
|
-
'aria-label':
|
|
359
|
+
'aria-label': displayNames.of(segment.type),
|
|
350
360
|
'aria-labelledby': `${ariaLabelledBy} ${id}`,
|
|
351
361
|
'aria-describedby': ariaDescribedBy,
|
|
352
362
|
'aria-placeholder': segment.isPlaceholder ? segment.text : undefined,
|
|
353
|
-
'aria-readonly':
|
|
363
|
+
'aria-readonly': state.isReadOnly || !segment.isEditable ? 'true' : undefined,
|
|
354
364
|
contentEditable: isEditable,
|
|
355
365
|
suppressContentEditableWarning: isEditable,
|
|
356
366
|
spellCheck: isEditable ? 'false' : undefined,
|
|
@@ -358,10 +368,13 @@ export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> &
|
|
|
358
368
|
autoCorrect: isEditable ? 'off' : undefined,
|
|
359
369
|
// Capitalization was changed in React 17...
|
|
360
370
|
[parseInt(React.version, 10) >= 17 ? 'enterKeyHint' : 'enterkeyhint']: isEditable ? 'next' : undefined,
|
|
361
|
-
inputMode:
|
|
362
|
-
tabIndex:
|
|
371
|
+
inputMode: state.isDisabled || segment.type === 'dayPeriod' || !isEditable ? undefined : 'numeric',
|
|
372
|
+
tabIndex: state.isDisabled ? undefined : 0,
|
|
363
373
|
onKeyDown,
|
|
364
|
-
onFocus
|
|
374
|
+
onFocus,
|
|
375
|
+
style: {
|
|
376
|
+
caretColor: 'transparent'
|
|
377
|
+
}
|
|
365
378
|
})
|
|
366
379
|
};
|
|
367
380
|
}
|