react-aria
Version:
Spectrum UI components in React
390 lines (366 loc) • 18.1 kB
JavaScript
var $da02ee888921bc9e$exports = require("../utils/shadowdom/DOMFunctions.cjs");
var $d865e4ff74ef4a73$exports = require("../utils/getScrollParent.cjs");
var $f6a0574fa98a3f25$exports = require("./useDateField.cjs");
var $d0b4a781cf26e80b$exports = require("../utils/platform.cjs");
var $89b39774f3b79dbb$exports = require("../utils/mergeProps.cjs");
var $9a1324d6ffd8bbb0$exports = require("../utils/scrollIntoView.cjs");
var $b8ffb0adc97ab95f$exports = require("../i18n/useDateFormatter.cjs");
var $8c5a06cee2b48b29$exports = require("./useDisplayNames.cjs");
var $6e76e65001bbcda2$exports = require("../utils/useEvent.cjs");
var $c4ede287f8af6ae2$exports = require("../i18n/useFilter.cjs");
var $7ac82d1fee77eb8a$exports = require("../utils/useId.cjs");
var $3f0180db35edfbf7$exports = require("../utils/useLabels.cjs");
var $429333cab433657c$exports = require("../utils/useLayoutEffect.cjs");
var $2522e612fa919664$exports = require("../i18n/I18nProvider.cjs");
var $fcf004be7e8533a5$exports = require("../spinbutton/useSpinButton.cjs");
var $5hSAi$internationalizeddate = require("@internationalized/date");
var $5hSAi$internationalizednumber = require("@internationalized/number");
var $5hSAi$react = require("react");
function $parcel$interopDefault(a) {
return a && a.__esModule ? a.default : a;
}
function $parcel$export(e, n, v, s) {
Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
}
$parcel$export(module.exports, "useDateSegment", function () { return $76567d92a57bde85$export$1315d136e6f7581; });
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
function $76567d92a57bde85$export$1315d136e6f7581(segment, state, ref) {
let enteredKeys = (0, $5hSAi$react.useRef)('');
let { locale: locale, direction: direction } = (0, $2522e612fa919664$exports.useLocale)();
let displayNames = (0, $8c5a06cee2b48b29$exports.useDisplayNames)();
let { ariaLabel: ariaLabel, ariaLabelledBy: ariaLabelledBy, ariaDescribedBy: ariaDescribedBy, focusManager: focusManager } = (0, $f6a0574fa98a3f25$exports.hookData).get(state);
let textValue = segment.isPlaceholder ? '' : segment.text;
let options = (0, $5hSAi$react.useMemo)(()=>state.dateFormatter.resolvedOptions(), [
state.dateFormatter
]);
let monthDateFormatter = (0, $b8ffb0adc97ab95f$exports.useDateFormatter)({
month: 'long',
timeZone: options.timeZone
});
let hourDateFormatter = (0, $b8ffb0adc97ab95f$exports.useDateFormatter)({
hour: 'numeric',
hour12: options.hour12,
timeZone: options.timeZone
});
if (segment.type === 'month' && !segment.isPlaceholder) {
let monthTextValue = monthDateFormatter.format(state.dateValue);
textValue = monthTextValue !== textValue ? `${textValue} \u{2013} ${monthTextValue}` : monthTextValue;
} else if (segment.type === 'hour' && !segment.isPlaceholder) textValue = hourDateFormatter.format(state.dateValue);
let { spinButtonProps: spinButtonProps } = (0, $fcf004be7e8533a5$exports.useSpinButton)({
// The ARIA spec says aria-valuenow is optional if there's no value, but aXe seems to require it.
// This doesn't seem to have any negative effects with real AT since we also use aria-valuetext.
// https://github.com/dequelabs/axe-core/issues/3505
value: segment.value ?? undefined,
textValue: textValue,
minValue: segment.minValue,
maxValue: segment.maxValue,
isDisabled: state.isDisabled,
isReadOnly: state.isReadOnly || !segment.isEditable,
isRequired: state.isRequired,
onIncrement: ()=>{
enteredKeys.current = '';
state.increment(segment.type);
},
onDecrement: ()=>{
enteredKeys.current = '';
state.decrement(segment.type);
},
onIncrementPage: ()=>{
enteredKeys.current = '';
state.incrementPage(segment.type);
},
onDecrementPage: ()=>{
enteredKeys.current = '';
state.decrementPage(segment.type);
},
onIncrementToMax: ()=>{
enteredKeys.current = '';
state.incrementToMax(segment.type);
},
onDecrementToMin: ()=>{
enteredKeys.current = '';
state.decrementToMin(segment.type);
}
});
let parser = (0, $5hSAi$react.useMemo)(()=>new (0, $5hSAi$internationalizednumber.NumberParser)(locale, {
maximumFractionDigits: 0
}), [
locale
]);
let backspace = ()=>{
if (segment.text === segment.placeholder) focusManager.focusPrevious();
if (parser.isValidPartialNumber(segment.text) && !state.isReadOnly && !segment.isPlaceholder) {
let newValue = segment.text.slice(0, -1);
let parsed = parser.parse(newValue);
newValue = parsed === 0 ? '' : newValue;
if (newValue.length === 0 || parsed === 0) state.clearSegment(segment.type);
else state.setSegment(segment.type, parsed);
enteredKeys.current = newValue;
} else if (segment.type === 'dayPeriod' || segment.type === 'era') state.clearSegment(segment.type);
};
let onKeyDown = (e)=>{
// Firefox does not fire selectstart for Ctrl/Cmd + A
// https://bugzilla.mozilla.org/show_bug.cgi?id=1742153
if (e.key === 'a' && ((0, $d0b4a781cf26e80b$exports.isMac)() ? e.metaKey : e.ctrlKey)) e.preventDefault();
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
switch(e.key){
case 'Backspace':
case 'Delete':
// Safari on iOS does not fire beforeinput for the backspace key because the cursor is at the start.
e.preventDefault();
e.stopPropagation();
backspace();
break;
}
};
// Safari dayPeriod option doesn't work...
let { startsWith: startsWith } = (0, $c4ede287f8af6ae2$exports.useFilter)({
sensitivity: 'base'
});
let amPmFormatter = (0, $b8ffb0adc97ab95f$exports.useDateFormatter)({
hour: 'numeric',
hour12: true
});
let am = (0, $5hSAi$react.useMemo)(()=>{
let date = new Date();
date.setHours(0);
return amPmFormatter.formatToParts(date).find((part)=>part.type === 'dayPeriod').value;
}, [
amPmFormatter
]);
let pm = (0, $5hSAi$react.useMemo)(()=>{
let date = new Date();
date.setHours(12);
return amPmFormatter.formatToParts(date).find((part)=>part.type === 'dayPeriod').value;
}, [
amPmFormatter
]);
// Get a list of formatted era names so users can type the first character to choose one.
let eraFormatter = (0, $b8ffb0adc97ab95f$exports.useDateFormatter)({
year: 'numeric',
era: 'narrow',
timeZone: 'UTC'
});
let eras = (0, $5hSAi$react.useMemo)(()=>{
if (segment.type !== 'era') return [];
let date = (0, $5hSAi$internationalizeddate.toCalendar)(new (0, $5hSAi$internationalizeddate.CalendarDate)(1, 1, 1), state.calendar);
let eras = state.calendar.getEras().map((era)=>{
let eraDate = date.set({
year: 1,
month: 1,
day: 1,
era: era
}).toDate('UTC');
let parts = eraFormatter.formatToParts(eraDate);
let formatted = parts.find((p)=>p.type === 'era').value;
return {
era: era,
formatted: formatted
};
});
// Remove the common prefix from formatted values. This is so that in calendars with eras like
// ERA0 and ERA1 (e.g. Ethiopic), users can press "0" and "1" to select an era. In other cases,
// the first letter is used.
let prefixLength = $76567d92a57bde85$var$commonPrefixLength(eras.map((era)=>era.formatted));
if (prefixLength) for (let era of eras)era.formatted = era.formatted.slice(prefixLength);
return eras;
}, [
eraFormatter,
state.calendar,
segment.type
]);
let onInput = (key)=>{
if (state.isDisabled || state.isReadOnly) return;
let newValue = enteredKeys.current + key;
switch(segment.type){
case 'dayPeriod':
if (startsWith(am, key)) state.setSegment('dayPeriod', 0);
else if (startsWith(pm, key)) state.setSegment('dayPeriod', 1);
else break;
focusManager.focusNext();
break;
case 'era':
{
let matched = eras.find((e)=>startsWith(e.formatted, key));
if (matched) {
state.setSegment('era', matched.era);
focusManager.focusNext();
}
break;
}
case 'day':
case 'hour':
case 'minute':
case 'second':
case 'month':
case 'year':
{
if (!parser.isValidPartialNumber(newValue)) return;
let numberValue = parser.parse(newValue);
let segmentValue = numberValue;
if (segment.maxValue !== undefined && numberValue > segment.maxValue) segmentValue = parser.parse(key);
if (isNaN(numberValue)) return;
state.setSegment(segment.type, segmentValue);
if (segment.maxValue !== undefined && (Number(numberValue + '0') > segment.maxValue || newValue.length >= String(segment.maxValue).length)) {
enteredKeys.current = '';
focusManager.focusNext();
} else enteredKeys.current = newValue;
break;
}
}
};
let onFocus = ()=>{
enteredKeys.current = '';
if (ref.current) (0, $9a1324d6ffd8bbb0$exports.scrollIntoViewport)(ref.current, {
containingElement: (0, $d865e4ff74ef4a73$exports.getScrollParent)(ref.current)
});
// Collapse selection to start or Chrome won't fire input events.
let selection = window.getSelection();
selection?.collapse(ref.current);
};
let documentRef = (0, $5hSAi$react.useRef)(typeof document !== 'undefined' ? document : null);
(0, $6e76e65001bbcda2$exports.useEvent)(documentRef, 'selectionchange', ()=>{
// Enforce that the selection is collapsed when inside a date segment.
// Otherwise, when tapping on a segment in Android Chrome and then entering text,
// composition events will be fired that break the DOM structure and crash the page.
let selection = window.getSelection();
if (selection?.anchorNode && (0, $da02ee888921bc9e$exports.nodeContains)(ref.current, selection?.anchorNode)) selection.collapse(ref.current);
});
let compositionRef = (0, $5hSAi$react.useRef)('');
(0, $6e76e65001bbcda2$exports.useEvent)(ref, 'beforeinput', (e)=>{
if (!ref.current) return;
e.preventDefault();
switch(e.inputType){
case 'deleteContentBackward':
case 'deleteContentForward':
if (parser.isValidPartialNumber(segment.text) && !state.isReadOnly) backspace();
break;
case 'insertCompositionText':
// insertCompositionText cannot be canceled.
// Record the current state of the element so we can restore it in the `input` event below.
compositionRef.current = ref.current.textContent;
// Safari gets stuck in a composition state unless we also assign to the value here.
// eslint-disable-next-line no-self-assign
ref.current.textContent = ref.current.textContent;
break;
default:
if (e.data != null) onInput(e.data);
break;
}
});
(0, $6e76e65001bbcda2$exports.useEvent)(ref, 'input', (e)=>{
let { inputType: inputType, data: data } = e;
switch(inputType){
case 'insertCompositionText':
// Reset the DOM to how it was in the beforeinput event.
if (ref.current) ref.current.textContent = compositionRef.current;
// Android sometimes fires key presses of letters as composition events. Need to handle am/pm keys here too.
// Can also happen e.g. with Pinyin keyboard on iOS.
if (data != null && (startsWith(am, data) || startsWith(pm, data))) onInput(data);
break;
}
});
(0, $429333cab433657c$exports.useLayoutEffect)(()=>{
let element = ref.current;
return ()=>{
// If the focused segment is removed, focus the previous one, or the next one if there was no previous one.
if ((0, $da02ee888921bc9e$exports.getActiveElement)() === element) {
let prev = focusManager.focusPrevious();
if (!prev) focusManager.focusNext();
}
};
}, [
ref,
focusManager
]);
// spinbuttons cannot be focused with VoiceOver on iOS.
let touchPropOverrides = (0, $d0b4a781cf26e80b$exports.isIOS)() || segment.type === 'timeZoneName' ? {
role: 'textbox',
'aria-valuemax': null,
'aria-valuemin': null,
'aria-valuetext': null,
'aria-valuenow': null
} : {};
// Only apply aria-describedby to the first segment, unless the field is invalid. This avoids it being
// read every time the user navigates to a new segment.
let firstSegment = (0, $5hSAi$react.useMemo)(()=>state.segments.find((s)=>s.isEditable), [
state.segments
]);
if (segment !== firstSegment && !state.isInvalid) ariaDescribedBy = undefined;
let id = (0, $7ac82d1fee77eb8a$exports.useId)();
let isEditable = !state.isDisabled && !state.isReadOnly && segment.isEditable;
// Prepend the label passed from the field to each segment name.
// This is needed because VoiceOver on iOS does not announce groups.
let name = segment.type === 'literal' ? '' : displayNames.of(segment.type);
let labelProps = (0, $3f0180db35edfbf7$exports.useLabels)({
'aria-label': `${name}${ariaLabel ? `, ${ariaLabel}` : ''}${ariaLabelledBy ? ', ' : ''}`,
'aria-labelledby': ariaLabelledBy
});
// Literal segments should not be visible to screen readers. We don't really need any of the above,
// but the rules of hooks mean hooks cannot be conditional so we have to put this condition here.
if (segment.type === 'literal') return {
segmentProps: {
'aria-hidden': true
}
};
let segmentStyle = {
caretColor: 'transparent'
};
if (direction === 'rtl') {
// While the bidirectional algorithm seems to work properly on inline elements with actual values, it returns different results for placeholder strings.
// To ensure placeholder render in correct format, we apply the CSS equivalent of LRE (left-to-right embedding). See https://www.unicode.org/reports/tr9/#Explicit_Directional_Embeddings.
// However, we apply this to both placeholders and date segments with an actual value because the date segments will shift around when deleting otherwise.
segmentStyle.unicodeBidi = 'embed';
let format = options[segment.type];
if (format === 'numeric' || format === '2-digit') segmentStyle.direction = 'ltr';
}
return {
segmentProps: (0, $89b39774f3b79dbb$exports.mergeProps)(spinButtonProps, labelProps, {
id: id,
...touchPropOverrides,
'aria-invalid': state.isInvalid ? 'true' : undefined,
'aria-describedby': ariaDescribedBy,
'aria-readonly': state.isReadOnly || !segment.isEditable ? 'true' : undefined,
'data-placeholder': segment.isPlaceholder || undefined,
contentEditable: isEditable,
suppressContentEditableWarning: isEditable,
spellCheck: isEditable ? 'false' : undefined,
autoCorrect: isEditable ? 'off' : undefined,
// Capitalization was changed in React 17...
[parseInt((0, ($parcel$interopDefault($5hSAi$react))).version, 10) >= 17 ? 'enterKeyHint' : 'enterkeyhint']: isEditable ? 'next' : undefined,
inputMode: state.isDisabled || segment.type === 'dayPeriod' || segment.type === 'era' || !isEditable ? undefined : 'numeric',
tabIndex: state.isDisabled ? undefined : 0,
onKeyDown: onKeyDown,
onFocus: onFocus,
style: segmentStyle,
// Prevent pointer events from reaching useDatePickerGroup, and allow native browser behavior to focus the segment.
onPointerDown (e) {
e.stopPropagation();
},
onMouseDown (e) {
e.stopPropagation();
}
})
};
}
function $76567d92a57bde85$var$commonPrefixLength(strings) {
// Sort the strings, and compare the characters in the first and last to find the common prefix.
strings.sort();
let first = strings[0];
let last = strings[strings.length - 1];
for(let i = 0; i < first.length; i++){
if (first[i] !== last[i]) return i;
}
return 0;
}
//# sourceMappingURL=useDateSegment.cjs.map