UNPKG

react-aria

Version:
390 lines (366 loc) • 18.1 kB
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