@react-aria/datepicker 3.0.0-alpha.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/LICENSE +201 -0
- package/README.md +3 -0
- package/dist/main.js +923 -0
- package/dist/main.js.map +1 -0
- package/dist/module.js +859 -0
- package/dist/module.js.map +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +43 -0
- package/src/index.ts +17 -0
- package/src/useDateField.ts +72 -0
- package/src/useDatePicker.ts +87 -0
- package/src/useDatePickerGroup.ts +45 -0
- package/src/useDateRangePicker.ts +112 -0
- package/src/useDateSegment.ts +361 -0
- package/src/useDisplayNames.ts +50 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {AriaButtonProps} from '@react-types/button';
|
|
14
|
+
import {AriaDatePickerProps, AriaDateRangePickerProps, DateValue} from '@react-types/datepicker';
|
|
15
|
+
import {AriaDialogProps} from '@react-types/dialog';
|
|
16
|
+
import {createFocusManager} from '@react-aria/focus';
|
|
17
|
+
import {DateRangePickerState} from '@react-stately/datepicker';
|
|
18
|
+
import {HTMLAttributes, LabelHTMLAttributes, RefObject} from 'react';
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
import intlMessages from '../intl/*.json';
|
|
21
|
+
import {mergeProps, useDescription, useId, useLabels} from '@react-aria/utils';
|
|
22
|
+
import {useDatePickerGroup} from './useDatePickerGroup';
|
|
23
|
+
import {useField} from '@react-aria/label';
|
|
24
|
+
import {useFocusWithin} from '@react-aria/interactions';
|
|
25
|
+
import {useLocale, useMessageFormatter} from '@react-aria/i18n';
|
|
26
|
+
|
|
27
|
+
interface DateRangePickerAria<T extends DateValue> {
|
|
28
|
+
labelProps: LabelHTMLAttributes<HTMLLabelElement>,
|
|
29
|
+
groupProps: HTMLAttributes<HTMLElement>,
|
|
30
|
+
startFieldProps: AriaDatePickerProps<T>,
|
|
31
|
+
endFieldProps: AriaDatePickerProps<T>,
|
|
32
|
+
/** Props for the description element, if any. */
|
|
33
|
+
descriptionProps: HTMLAttributes<HTMLElement>,
|
|
34
|
+
/** Props for the error message element, if any. */
|
|
35
|
+
errorMessageProps: HTMLAttributes<HTMLElement>,
|
|
36
|
+
buttonProps: AriaButtonProps,
|
|
37
|
+
dialogProps: AriaDialogProps
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePickerProps<T>, state: DateRangePickerState, ref: RefObject<HTMLElement>): DateRangePickerAria<T> {
|
|
41
|
+
let formatMessage = useMessageFormatter(intlMessages);
|
|
42
|
+
let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({
|
|
43
|
+
...props,
|
|
44
|
+
labelElementType: 'span'
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
let labelledBy = fieldProps['aria-labelledby'] || fieldProps.id;
|
|
48
|
+
|
|
49
|
+
let {locale} = useLocale();
|
|
50
|
+
let description = state.formatValue(locale, {month: 'long'});
|
|
51
|
+
let descProps = useDescription(description);
|
|
52
|
+
|
|
53
|
+
let startFieldProps = useLabels({
|
|
54
|
+
'aria-label': formatMessage('startDate'),
|
|
55
|
+
'aria-labelledby': labelledBy
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let endFieldProps = useLabels({
|
|
59
|
+
'aria-label': formatMessage('endDate'),
|
|
60
|
+
'aria-labelledby': labelledBy
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let buttonId = useId();
|
|
64
|
+
let dialogId = useId();
|
|
65
|
+
|
|
66
|
+
let groupProps = useDatePickerGroup(state, ref);
|
|
67
|
+
let {focusWithinProps} = useFocusWithin({
|
|
68
|
+
onBlurWithin() {
|
|
69
|
+
state.confirmPlaceholder();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let ariaDescribedBy = [descProps['aria-describedby'], fieldProps['aria-describedby']].filter(Boolean).join(' ') || undefined;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
groupProps: mergeProps(groupProps, fieldProps, descProps, focusWithinProps, {
|
|
77
|
+
role: 'group',
|
|
78
|
+
'aria-disabled': props.isDisabled || null,
|
|
79
|
+
'aria-describedby': ariaDescribedBy
|
|
80
|
+
}),
|
|
81
|
+
labelProps: {
|
|
82
|
+
...labelProps,
|
|
83
|
+
onClick: () => {
|
|
84
|
+
let focusManager = createFocusManager(ref);
|
|
85
|
+
focusManager.focusFirst();
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
buttonProps: {
|
|
89
|
+
...descProps,
|
|
90
|
+
id: buttonId,
|
|
91
|
+
excludeFromTabOrder: true,
|
|
92
|
+
'aria-haspopup': 'dialog',
|
|
93
|
+
'aria-label': formatMessage('calendar'),
|
|
94
|
+
'aria-labelledby': `${labelledBy} ${buttonId}`,
|
|
95
|
+
'aria-describedby': ariaDescribedBy
|
|
96
|
+
},
|
|
97
|
+
dialogProps: {
|
|
98
|
+
id: dialogId,
|
|
99
|
+
'aria-labelledby': `${labelledBy} ${buttonId}`
|
|
100
|
+
},
|
|
101
|
+
startFieldProps: {
|
|
102
|
+
...startFieldProps,
|
|
103
|
+
'aria-describedby': fieldProps['aria-describedby']
|
|
104
|
+
},
|
|
105
|
+
endFieldProps: {
|
|
106
|
+
...endFieldProps,
|
|
107
|
+
'aria-describedby': fieldProps['aria-describedby']
|
|
108
|
+
},
|
|
109
|
+
descriptionProps,
|
|
110
|
+
errorMessageProps
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {DatePickerFieldState, DateSegment} from '@react-stately/datepicker';
|
|
14
|
+
import {DatePickerProps, DateValue} from '@react-types/datepicker';
|
|
15
|
+
import {DOMProps} from '@react-types/shared';
|
|
16
|
+
import {isIOS, isMac, mergeProps, useEvent, useId} from '@react-aria/utils';
|
|
17
|
+
import {labelIds} from './useDateField';
|
|
18
|
+
import {NumberParser} from '@internationalized/number';
|
|
19
|
+
import React, {HTMLAttributes, RefObject, useMemo, useRef} from 'react';
|
|
20
|
+
import {useDateFormatter, useFilter, useLocale} from '@react-aria/i18n';
|
|
21
|
+
import {useDisplayNames} from './useDisplayNames';
|
|
22
|
+
import {useFocusManager} from '@react-aria/focus';
|
|
23
|
+
import {usePress} from '@react-aria/interactions';
|
|
24
|
+
import {useSpinButton} from '@react-aria/spinbutton';
|
|
25
|
+
|
|
26
|
+
interface DateSegmentAria {
|
|
27
|
+
segmentProps: HTMLAttributes<HTMLDivElement>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useDateSegment<T extends DateValue>(props: DatePickerProps<T> & DOMProps, segment: DateSegment, state: DatePickerFieldState, ref: RefObject<HTMLElement>): DateSegmentAria {
|
|
31
|
+
let enteredKeys = useRef('');
|
|
32
|
+
let {locale, direction} = useLocale();
|
|
33
|
+
let displayNames = useDisplayNames();
|
|
34
|
+
let focusManager = useFocusManager();
|
|
35
|
+
|
|
36
|
+
let textValue = segment.text;
|
|
37
|
+
let options = useMemo(() => state.dateFormatter.resolvedOptions(), [state.dateFormatter]);
|
|
38
|
+
let monthDateFormatter = useDateFormatter({month: 'long', timeZone: options.timeZone});
|
|
39
|
+
let hourDateFormatter = useDateFormatter({
|
|
40
|
+
hour: 'numeric',
|
|
41
|
+
hour12: options.hour12,
|
|
42
|
+
timeZone: options.timeZone
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (segment.type === 'month') {
|
|
46
|
+
let monthTextValue = monthDateFormatter.format(state.dateValue);
|
|
47
|
+
textValue = monthTextValue !== textValue ? `${textValue} - ${monthTextValue}` : monthTextValue;
|
|
48
|
+
} else if (segment.type === 'hour' || segment.type === 'dayPeriod') {
|
|
49
|
+
textValue = hourDateFormatter.format(state.dateValue);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let {spinButtonProps} = useSpinButton({
|
|
53
|
+
value: segment.value,
|
|
54
|
+
textValue,
|
|
55
|
+
minValue: segment.minValue,
|
|
56
|
+
maxValue: segment.maxValue,
|
|
57
|
+
isDisabled: props.isDisabled,
|
|
58
|
+
isReadOnly: props.isReadOnly || !segment.isEditable,
|
|
59
|
+
isRequired: props.isRequired,
|
|
60
|
+
onIncrement: () => {
|
|
61
|
+
enteredKeys.current = '';
|
|
62
|
+
state.increment(segment.type);
|
|
63
|
+
},
|
|
64
|
+
onDecrement: () => {
|
|
65
|
+
enteredKeys.current = '';
|
|
66
|
+
state.decrement(segment.type);
|
|
67
|
+
},
|
|
68
|
+
onIncrementPage: () => {
|
|
69
|
+
enteredKeys.current = '';
|
|
70
|
+
state.incrementPage(segment.type);
|
|
71
|
+
},
|
|
72
|
+
onDecrementPage: () => {
|
|
73
|
+
enteredKeys.current = '';
|
|
74
|
+
state.decrementPage(segment.type);
|
|
75
|
+
},
|
|
76
|
+
onIncrementToMax: () => {
|
|
77
|
+
enteredKeys.current = '';
|
|
78
|
+
state.setSegment(segment.type, segment.maxValue);
|
|
79
|
+
},
|
|
80
|
+
onDecrementToMin: () => {
|
|
81
|
+
enteredKeys.current = '';
|
|
82
|
+
state.setSegment(segment.type, segment.minValue);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
let parser = useMemo(() => new NumberParser(locale, {maximumFractionDigits: 0}), [locale]);
|
|
87
|
+
|
|
88
|
+
let backspace = () => {
|
|
89
|
+
if (parser.isValidPartialNumber(segment.text) && !props.isReadOnly && !segment.isPlaceholder) {
|
|
90
|
+
let newValue = segment.text.slice(0, -1);
|
|
91
|
+
let parsed = parser.parse(newValue);
|
|
92
|
+
if (newValue.length === 0 || parsed === 0) {
|
|
93
|
+
state.clearSegment(segment.type);
|
|
94
|
+
} else {
|
|
95
|
+
state.setSegment(segment.type, parsed);
|
|
96
|
+
}
|
|
97
|
+
enteredKeys.current = newValue;
|
|
98
|
+
} else if (segment.type === 'dayPeriod') {
|
|
99
|
+
state.clearSegment(segment.type);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let onKeyDown = (e) => {
|
|
104
|
+
// Firefox does not fire selectstart for Ctrl/Cmd + A
|
|
105
|
+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1742153
|
|
106
|
+
if (e.key === 'a' && (isMac() ? e.metaKey : e.ctrlKey)) {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
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
|
+
case 'Backspace':
|
|
144
|
+
case 'Delete': {
|
|
145
|
+
// Safari on iOS does not fire beforeinput for the backspace key because the cursor is at the start.
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
e.stopPropagation();
|
|
148
|
+
backspace();
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Safari dayPeriod option doesn't work...
|
|
155
|
+
let {startsWith} = useFilter({sensitivity: 'base'});
|
|
156
|
+
let amPmFormatter = useDateFormatter({hour: 'numeric', hour12: true});
|
|
157
|
+
let am = useMemo(() => {
|
|
158
|
+
let date = new Date();
|
|
159
|
+
date.setHours(0);
|
|
160
|
+
return amPmFormatter.formatToParts(date).find(part => part.type === 'dayPeriod').value;
|
|
161
|
+
}, [amPmFormatter]);
|
|
162
|
+
|
|
163
|
+
let pm = useMemo(() => {
|
|
164
|
+
let date = new Date();
|
|
165
|
+
date.setHours(12);
|
|
166
|
+
return amPmFormatter.formatToParts(date).find(part => part.type === 'dayPeriod').value;
|
|
167
|
+
}, [amPmFormatter]);
|
|
168
|
+
|
|
169
|
+
let onInput = (key: string) => {
|
|
170
|
+
if (props.isDisabled || props.isReadOnly) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let newValue = enteredKeys.current + key;
|
|
175
|
+
|
|
176
|
+
switch (segment.type) {
|
|
177
|
+
case 'dayPeriod':
|
|
178
|
+
if (startsWith(am, key)) {
|
|
179
|
+
state.setSegment('dayPeriod', 0);
|
|
180
|
+
} else if (startsWith(pm, key)) {
|
|
181
|
+
state.setSegment('dayPeriod', 12);
|
|
182
|
+
} else {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
focusManager.focusNext();
|
|
186
|
+
break;
|
|
187
|
+
case 'day':
|
|
188
|
+
case 'hour':
|
|
189
|
+
case 'minute':
|
|
190
|
+
case 'second':
|
|
191
|
+
case 'month':
|
|
192
|
+
case 'year': {
|
|
193
|
+
if (!parser.isValidPartialNumber(newValue)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let numberValue = parser.parse(newValue);
|
|
198
|
+
let segmentValue = numberValue;
|
|
199
|
+
let allowsZero = segment.minValue === 0;
|
|
200
|
+
if (segment.type === 'hour' && state.dateFormatter.resolvedOptions().hour12) {
|
|
201
|
+
switch (state.dateFormatter.resolvedOptions().hourCycle) {
|
|
202
|
+
case 'h11':
|
|
203
|
+
if (numberValue > 11) {
|
|
204
|
+
segmentValue = parser.parse(key);
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
case 'h12':
|
|
208
|
+
allowsZero = false;
|
|
209
|
+
if (numberValue > 12) {
|
|
210
|
+
segmentValue = parser.parse(key);
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (segment.value >= 12 && numberValue > 1) {
|
|
216
|
+
numberValue += 12;
|
|
217
|
+
}
|
|
218
|
+
} else if (numberValue > segment.maxValue) {
|
|
219
|
+
segmentValue = parser.parse(key);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (isNaN(numberValue)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let shouldSetValue = segmentValue !== 0 || allowsZero;
|
|
227
|
+
if (shouldSetValue) {
|
|
228
|
+
state.setSegment(segment.type, segmentValue);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (Number(numberValue + '0') > segment.maxValue || newValue.length >= String(segment.maxValue).length) {
|
|
232
|
+
enteredKeys.current = '';
|
|
233
|
+
if (shouldSetValue) {
|
|
234
|
+
focusManager.focusNext();
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
enteredKeys.current = newValue;
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
let onFocus = () => {
|
|
245
|
+
enteredKeys.current = '';
|
|
246
|
+
if (ref.current?.scrollIntoView) {
|
|
247
|
+
ref.current.scrollIntoView();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Safari requires that a selection is set or it won't fire input events.
|
|
251
|
+
// Since usePress disables text selection, this won't happen by default.
|
|
252
|
+
ref.current.style.webkitUserSelect = 'text';
|
|
253
|
+
let selection = window.getSelection();
|
|
254
|
+
selection.collapse(ref.current);
|
|
255
|
+
ref.current.style.webkitUserSelect = '';
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
let compositionRef = useRef('');
|
|
259
|
+
// @ts-ignore - TODO: possibly old TS version? doesn't fail in my editor...
|
|
260
|
+
useEvent(ref, 'beforeinput', e => {
|
|
261
|
+
e.preventDefault();
|
|
262
|
+
|
|
263
|
+
switch (e.inputType) {
|
|
264
|
+
case 'deleteContentBackward':
|
|
265
|
+
case 'deleteContentForward':
|
|
266
|
+
if (parser.isValidPartialNumber(segment.text) && !props.isReadOnly) {
|
|
267
|
+
backspace();
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
case 'insertCompositionText':
|
|
271
|
+
// insertCompositionText cannot be canceled.
|
|
272
|
+
// Record the current state of the element so we can restore it in the `input` event below.
|
|
273
|
+
compositionRef.current = ref.current.textContent;
|
|
274
|
+
|
|
275
|
+
// Safari gets stuck in a composition state unless we also assign to the value here.
|
|
276
|
+
// eslint-disable-next-line no-self-assign
|
|
277
|
+
ref.current.textContent = ref.current.textContent;
|
|
278
|
+
break;
|
|
279
|
+
default:
|
|
280
|
+
if (e.data != null) {
|
|
281
|
+
onInput(e.data);
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
useEvent(ref, 'input', (e: InputEvent) => {
|
|
288
|
+
let {inputType, data} = e;
|
|
289
|
+
switch (inputType) {
|
|
290
|
+
case 'insertCompositionText':
|
|
291
|
+
// Reset the DOM to how it was in the beforeinput event.
|
|
292
|
+
ref.current.textContent = compositionRef.current;
|
|
293
|
+
|
|
294
|
+
// Android sometimes fires key presses of letters as composition events. Need to handle am/pm keys here too.
|
|
295
|
+
// Can also happen e.g. with Pinyin keyboard on iOS.
|
|
296
|
+
if (startsWith(am, data) || startsWith(pm, data)) {
|
|
297
|
+
onInput(data);
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Focus on mouse down/touch up to match native textfield behavior.
|
|
304
|
+
// usePress handles canceling text selection.
|
|
305
|
+
let {pressProps} = usePress({
|
|
306
|
+
preventFocusOnPress: true,
|
|
307
|
+
onPressStart: (e) => {
|
|
308
|
+
if (e.pointerType === 'mouse') {
|
|
309
|
+
e.target.focus();
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
onPress(e) {
|
|
313
|
+
if (e.pointerType !== 'mouse') {
|
|
314
|
+
e.target.focus();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// For Android: prevent selection on long press.
|
|
320
|
+
useEvent(ref, 'selectstart', e => {
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// spinbuttons cannot be focused with VoiceOver on iOS.
|
|
325
|
+
let touchPropOverrides = isIOS() || segment.type === 'timeZoneName' ? {
|
|
326
|
+
role: 'textbox',
|
|
327
|
+
'aria-valuemax': null,
|
|
328
|
+
'aria-valuemin': null,
|
|
329
|
+
'aria-valuetext': null,
|
|
330
|
+
'aria-valuenow': null
|
|
331
|
+
} : {};
|
|
332
|
+
|
|
333
|
+
let fieldLabelId = labelIds.get(state);
|
|
334
|
+
|
|
335
|
+
let id = useId(props.id);
|
|
336
|
+
let isEditable = !props.isDisabled && !props.isReadOnly && segment.isEditable;
|
|
337
|
+
return {
|
|
338
|
+
segmentProps: mergeProps(spinButtonProps, pressProps, {
|
|
339
|
+
id,
|
|
340
|
+
...touchPropOverrides,
|
|
341
|
+
'aria-controls': props['aria-controls'],
|
|
342
|
+
// 'aria-haspopup': props['aria-haspopup'], // deprecated in ARIA 1.2
|
|
343
|
+
'aria-invalid': state.validationState === 'invalid' ? 'true' : undefined,
|
|
344
|
+
'aria-label': segment.type !== 'literal' ? displayNames.of(segment.type) : undefined,
|
|
345
|
+
'aria-labelledby': `${fieldLabelId} ${id}`,
|
|
346
|
+
'aria-placeholder': segment.isPlaceholder ? segment.text : undefined,
|
|
347
|
+
'aria-readonly': props.isReadOnly || !segment.isEditable ? 'true' : undefined,
|
|
348
|
+
contentEditable: isEditable,
|
|
349
|
+
suppressContentEditableWarning: isEditable,
|
|
350
|
+
spellCheck: isEditable ? 'false' : undefined,
|
|
351
|
+
autoCapitalize: isEditable ? 'off' : undefined,
|
|
352
|
+
autoCorrect: isEditable ? 'off' : undefined,
|
|
353
|
+
// Capitalization was changed in React 17...
|
|
354
|
+
[parseInt(React.version, 10) >= 17 ? 'enterKeyHint' : 'enterkeyhint']: isEditable ? 'next' : undefined,
|
|
355
|
+
inputMode: props.isDisabled || segment.type === 'dayPeriod' || !isEditable ? undefined : 'numeric',
|
|
356
|
+
tabIndex: props.isDisabled ? undefined : 0,
|
|
357
|
+
onKeyDown,
|
|
358
|
+
onFocus
|
|
359
|
+
})
|
|
360
|
+
};
|
|
361
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2020 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
import intlMessages from '../intl/*.json';
|
|
15
|
+
import {MessageDictionary} from '@internationalized/message';
|
|
16
|
+
import {useLocale} from '@react-aria/i18n';
|
|
17
|
+
import {useMemo} from 'react';
|
|
18
|
+
|
|
19
|
+
type Field = 'era' | 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'dayPeriod' | 'timeZoneName' | 'weekday';
|
|
20
|
+
interface DisplayNames {
|
|
21
|
+
of(field: Field): string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useDisplayNames(): DisplayNames {
|
|
25
|
+
let {locale} = useLocale();
|
|
26
|
+
return useMemo(() => {
|
|
27
|
+
// Try to use Intl.DisplayNames if possible. It may be supported in browsers, but not support the dateTimeField
|
|
28
|
+
// type as that was only added in v2. https://github.com/tc39/intl-displaynames-v2
|
|
29
|
+
try {
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
return new Intl.DisplayNames(locale, {type: 'dateTimeField'});
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return new DisplayNamesPolyfill(locale);
|
|
34
|
+
}
|
|
35
|
+
}, [locale]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class DisplayNamesPolyfill implements DisplayNames {
|
|
39
|
+
private locale: string;
|
|
40
|
+
private dictionary: MessageDictionary;
|
|
41
|
+
|
|
42
|
+
constructor(locale: string) {
|
|
43
|
+
this.locale = locale;
|
|
44
|
+
this.dictionary = new MessageDictionary(intlMessages);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
of(field: Field): string {
|
|
48
|
+
return this.dictionary.getStringForLocale(field, this.locale);
|
|
49
|
+
}
|
|
50
|
+
}
|