UNPKG

@mui/x-date-pickers

Version:

The community edition of the Date and Time Picker components (MUI X).

419 lines (410 loc) 17.8 kB
"use strict"; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.useFieldV6TextField = exports.addPositionPropertiesToSections = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var React = _interopRequireWildcard(require("react")); var _RtlProvider = require("@mui/system/RtlProvider"); var _useEnhancedEffect = _interopRequireDefault(require("@mui/utils/useEnhancedEffect")); var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback")); var _useTimeout = _interopRequireDefault(require("@mui/utils/useTimeout")); var _useForkRef = _interopRequireDefault(require("@mui/utils/useForkRef")); var _hooks = require("../../../hooks"); var _utils = require("../../utils/utils"); var _useField = require("./useField.utils"); var _useFieldCharacterEditing = require("./useFieldCharacterEditing"); var _useFieldRootHandleKeyDown = require("./useFieldRootHandleKeyDown"); var _useFieldState = require("./useFieldState"); var _useFieldInternalPropsWithDefaults = require("./useFieldInternalPropsWithDefaults"); const cleanString = dirtyString => dirtyString.replace(/[\u2066\u2067\u2068\u2069]/g, ''); const addPositionPropertiesToSections = (sections, localizedDigits, isRtl) => { let position = 0; let positionInInput = isRtl ? 1 : 0; const newSections = []; for (let i = 0; i < sections.length; i += 1) { const section = sections[i]; const renderedValue = (0, _useField.getSectionVisibleValue)(section, isRtl ? 'input-rtl' : 'input-ltr', localizedDigits); const sectionStr = `${section.startSeparator}${renderedValue}${section.endSeparator}`; const sectionLength = cleanString(sectionStr).length; const sectionLengthInInput = sectionStr.length; // The ...InInput values consider the unicode characters but do include them in their indexes const cleanedValue = cleanString(renderedValue); const startInInput = positionInInput + (cleanedValue === '' ? 0 : renderedValue.indexOf(cleanedValue[0])) + section.startSeparator.length; const endInInput = startInInput + cleanedValue.length; newSections.push((0, _extends2.default)({}, section, { start: position, end: position + sectionLength, startInInput, endInInput })); position += sectionLength; // Move position to the end of string associated to the current section positionInInput += sectionLengthInInput; } return newSections; }; exports.addPositionPropertiesToSections = addPositionPropertiesToSections; const useFieldV6TextField = parameters => { const isRtl = (0, _RtlProvider.useRtl)(); const focusTimeout = (0, _useTimeout.default)(); const selectionSyncTimeout = (0, _useTimeout.default)(); const { props, manager, skipContextFieldRefAssignment, manager: { valueType, internal_valueManager: valueManager, internal_fieldValueManager: fieldValueManager, internal_useOpenPickerButtonAriaLabel: useOpenPickerButtonAriaLabel } } = parameters; const { internalProps, forwardedProps } = (0, _hooks.useSplitFieldProps)(props, valueType); const internalPropsWithDefaults = (0, _useFieldInternalPropsWithDefaults.useFieldInternalPropsWithDefaults)({ manager, internalProps, skipContextFieldRefAssignment }); const { onFocus, onClick, onPaste, onBlur, onKeyDown, onClear, clearable, inputRef: inputRefProp, placeholder: inPlaceholder } = forwardedProps; const { readOnly = false, disabled = false, autoFocus = false, focused, unstableFieldRef } = internalPropsWithDefaults; const inputRef = React.useRef(null); const handleRef = (0, _useForkRef.default)(inputRefProp, inputRef); const stateResponse = (0, _useFieldState.useFieldState)({ manager, internalPropsWithDefaults, forwardedProps }); const { // States and derived states activeSectionIndex, areAllSectionsEmpty, error, localizedDigits, parsedSelectedSections, sectionOrder, state, value, // Methods to update the states clearValue, clearActiveSection, setCharacterQuery, setSelectedSections, setTempAndroidValueStr, updateSectionValue, updateValueFromValueStr, // Utilities methods getSectionsFromValue } = stateResponse; const applyCharacterEditing = (0, _useFieldCharacterEditing.useFieldCharacterEditing)({ stateResponse }); const openPickerAriaLabel = useOpenPickerButtonAriaLabel(value); const sections = React.useMemo(() => addPositionPropertiesToSections(state.sections, localizedDigits, isRtl), [state.sections, localizedDigits, isRtl]); function syncSelectionFromDOM() { const browserStartIndex = inputRef.current.selectionStart ?? 0; let nextSectionIndex; if (browserStartIndex <= sections[0].startInInput) { // Special case if browser index is in invisible characters at the beginning nextSectionIndex = 1; } else if (browserStartIndex >= sections[sections.length - 1].endInInput) { // If the click is after the last character of the input, then we want to select the 1st section. nextSectionIndex = 1; } else { nextSectionIndex = sections.findIndex(section => section.startInInput - section.startSeparator.length > browserStartIndex); } const sectionIndex = nextSectionIndex === -1 ? sections.length - 1 : nextSectionIndex - 1; setSelectedSections(sectionIndex); } function focusField(newSelectedSection = 0) { if ((0, _utils.getActiveElement)(document) === inputRef.current) { return; } inputRef.current?.focus(); setSelectedSections(newSelectedSection); } const handleInputFocus = (0, _useEventCallback.default)(event => { onFocus?.(event); // The ref is guaranteed to be resolved at this point. const input = inputRef.current; focusTimeout.start(0, () => { // The ref changed, the component got remounted, the focus event is no longer relevant. if (!input || input !== inputRef.current) { return; } if (activeSectionIndex != null) { return; } if ( // avoid selecting all sections when focusing empty field without value input.value.length && Number(input.selectionEnd) - Number(input.selectionStart) === input.value.length) { setSelectedSections('all'); } else { syncSelectionFromDOM(); } }); }); const handleInputClick = (0, _useEventCallback.default)((event, ...args) => { // The click event on the clear button would propagate to the input, trigger this handler and result in a wrong section selection. // We avoid this by checking if the call of `handleInputClick` is actually intended, or a side effect. if (event.isDefaultPrevented()) { return; } onClick?.(event, ...args); syncSelectionFromDOM(); }); const handleInputPaste = (0, _useEventCallback.default)(event => { onPaste?.(event); // prevent default to avoid the input `onChange` handler being called event.preventDefault(); if (readOnly || disabled) { return; } const pastedValue = event.clipboardData.getData('text'); if (typeof parsedSelectedSections === 'number') { const activeSection = state.sections[parsedSelectedSections]; const lettersOnly = /^[a-zA-Z]+$/.test(pastedValue); const digitsOnly = /^[0-9]+$/.test(pastedValue); const digitsAndLetterOnly = /^(([a-zA-Z]+)|)([0-9]+)(([a-zA-Z]+)|)$/.test(pastedValue); const isValidPastedValue = activeSection.contentType === 'letter' && lettersOnly || activeSection.contentType === 'digit' && digitsOnly || activeSection.contentType === 'digit-with-letter' && digitsAndLetterOnly; if (isValidPastedValue) { setCharacterQuery(null); updateSectionValue({ section: activeSection, newSectionValue: pastedValue, shouldGoToNextSection: true }); return; } if (lettersOnly || digitsOnly) { // The pasted value corresponds to a single section, but not the expected type, // skip the modification return; } } setCharacterQuery(null); updateValueFromValueStr(pastedValue); }); const handleContainerBlur = (0, _useEventCallback.default)(event => { onBlur?.(event); setSelectedSections(null); }); const handleInputChange = (0, _useEventCallback.default)(event => { if (readOnly) { return; } const targetValue = event.target.value; if (targetValue === '') { clearValue(); return; } const eventData = event.nativeEvent.data; // Calling `.fill(04/11/2022)` in playwright will trigger a change event with the requested content to insert in `event.nativeEvent.data` // usual changes have only the currently typed character in the `event.nativeEvent.data` const shouldUseEventData = eventData && eventData.length > 1; const valueStr = shouldUseEventData ? eventData : targetValue; const cleanValueStr = cleanString(valueStr); if (parsedSelectedSections === 'all') { setSelectedSections(activeSectionIndex); } // If no section is selected or eventData should be used, we just try to parse the new value // This line is mostly triggered by imperative code / application tests. if (activeSectionIndex == null || shouldUseEventData) { updateValueFromValueStr(shouldUseEventData ? eventData : cleanValueStr); return; } let keyPressed; if (parsedSelectedSections === 'all' && cleanValueStr.length === 1) { keyPressed = cleanValueStr; } else { const prevValueStr = cleanString(fieldValueManager.getV6InputValueFromSections(sections, localizedDigits, isRtl)); let startOfDiffIndex = -1; let endOfDiffIndex = -1; for (let i = 0; i < prevValueStr.length; i += 1) { if (startOfDiffIndex === -1 && prevValueStr[i] !== cleanValueStr[i]) { startOfDiffIndex = i; } if (endOfDiffIndex === -1 && prevValueStr[prevValueStr.length - i - 1] !== cleanValueStr[cleanValueStr.length - i - 1]) { endOfDiffIndex = i; } } const activeSection = sections[activeSectionIndex]; const hasDiffOutsideOfActiveSection = startOfDiffIndex < activeSection.start || prevValueStr.length - endOfDiffIndex - 1 > activeSection.end; if (hasDiffOutsideOfActiveSection) { // TODO: Support if the new date is valid return; } // The active section being selected, the browser has replaced its value with the key pressed by the user. const activeSectionEndRelativeToNewValue = cleanValueStr.length - prevValueStr.length + activeSection.end - cleanString(activeSection.endSeparator || '').length; keyPressed = cleanValueStr.slice(activeSection.start + cleanString(activeSection.startSeparator || '').length, activeSectionEndRelativeToNewValue); } if (keyPressed.length === 0) { if ((0, _useField.isAndroid)()) { setTempAndroidValueStr(valueStr); } clearActiveSection(); return; } applyCharacterEditing({ keyPressed, sectionIndex: activeSectionIndex }); }); const handleClear = (0, _useEventCallback.default)((event, ...args) => { event.preventDefault(); onClear?.(event, ...args); clearValue(); if (!isFieldFocused(inputRef)) { // setSelectedSections is called internally focusField(0); } else { setSelectedSections(sectionOrder.startIndex); } }); const handleContainerKeyDown = (0, _useFieldRootHandleKeyDown.useFieldRootHandleKeyDown)({ manager, internalPropsWithDefaults, stateResponse }); const wrappedHandleContainerKeyDown = (0, _useEventCallback.default)(event => { onKeyDown?.(event); handleContainerKeyDown(event); }); const placeholder = React.useMemo(() => { if (inPlaceholder !== undefined) { return inPlaceholder; } return fieldValueManager.getV6InputValueFromSections(getSectionsFromValue(valueManager.emptyValue), localizedDigits, isRtl); }, [inPlaceholder, fieldValueManager, getSectionsFromValue, valueManager.emptyValue, localizedDigits, isRtl]); const valueStr = React.useMemo(() => state.tempValueStrAndroid ?? fieldValueManager.getV6InputValueFromSections(state.sections, localizedDigits, isRtl), [state.sections, fieldValueManager, state.tempValueStrAndroid, localizedDigits, isRtl]); React.useEffect(() => { // Select all the sections when focused on mount (`autoFocus = true` on the input) if (inputRef.current && inputRef.current === (0, _utils.getActiveElement)(document)) { setSelectedSections('all'); } }, []); // eslint-disable-line react-hooks/exhaustive-deps (0, _useEnhancedEffect.default)(() => { function syncSelectionToDOM() { if (!inputRef.current) { return; } if (parsedSelectedSections == null) { if (inputRef.current.scrollLeft) { // Ensure that input content is not marked as selected. // setting selection range to 0 causes issues in Safari. // https://bugs.webkit.org/show_bug.cgi?id=224425 inputRef.current.scrollLeft = 0; } return; } // On multi input range pickers we want to update selection range only for the active input // This helps to avoid the focus jumping on Safari https://github.com/mui/mui-x/issues/9003 // because WebKit implements the `setSelectionRange` based on the spec: https://bugs.webkit.org/show_bug.cgi?id=224425 if (inputRef.current !== (0, _utils.getActiveElement)(document)) { return; } // Fix scroll jumping on iOS browser: https://github.com/mui/mui-x/issues/8321 const currentScrollTop = inputRef.current.scrollTop; if (parsedSelectedSections === 'all') { inputRef.current.select(); } else { const selectedSection = sections[parsedSelectedSections]; const selectionStart = selectedSection.type === 'empty' ? selectedSection.startInInput - selectedSection.startSeparator.length : selectedSection.startInInput; const selectionEnd = selectedSection.type === 'empty' ? selectedSection.endInInput + selectedSection.endSeparator.length : selectedSection.endInInput; if (selectionStart !== inputRef.current.selectionStart || selectionEnd !== inputRef.current.selectionEnd) { if (inputRef.current === (0, _utils.getActiveElement)(document)) { inputRef.current.setSelectionRange(selectionStart, selectionEnd); } } selectionSyncTimeout.start(0, () => { // handle case when the selection is not updated correctly // could happen on Android if (inputRef.current && inputRef.current === (0, _utils.getActiveElement)(document) && // The section might loose all selection, where `selectionStart === selectionEnd` // https://github.com/mui/mui-x/pull/13652 inputRef.current.selectionStart === inputRef.current.selectionEnd && (inputRef.current.selectionStart !== selectionStart || inputRef.current.selectionEnd !== selectionEnd)) { syncSelectionToDOM(); } }); } // Even reading this variable seems to do the trick, but also setting it just to make use of it inputRef.current.scrollTop = currentScrollTop; } syncSelectionToDOM(); }); const inputMode = React.useMemo(() => { if (activeSectionIndex == null) { return 'text'; } if (state.sections[activeSectionIndex].contentType === 'letter') { return 'text'; } return 'numeric'; }, [activeSectionIndex, state.sections]); const inputHasFocus = inputRef.current && inputRef.current === (0, _utils.getActiveElement)(document); const shouldShowPlaceholder = !inputHasFocus && areAllSectionsEmpty; React.useImperativeHandle(unstableFieldRef, () => ({ getSections: () => state.sections, getActiveSectionIndex: () => { const browserStartIndex = inputRef.current.selectionStart ?? 0; const browserEndIndex = inputRef.current.selectionEnd ?? 0; if (browserStartIndex === 0 && browserEndIndex === 0) { return null; } const nextSectionIndex = browserStartIndex <= sections[0].startInInput ? 1 // Special case if browser index is in invisible characters at the beginning. : sections.findIndex(section => section.startInInput - section.startSeparator.length > browserStartIndex); return nextSectionIndex === -1 ? sections.length - 1 : nextSectionIndex - 1; }, setSelectedSections: newSelectedSections => setSelectedSections(newSelectedSections), focusField, isFieldFocused: () => isFieldFocused(inputRef) })); return (0, _extends2.default)({}, forwardedProps, { error, clearable: Boolean(clearable && !areAllSectionsEmpty && !readOnly && !disabled), onBlur: handleContainerBlur, onClick: handleInputClick, onFocus: handleInputFocus, onPaste: handleInputPaste, onKeyDown: wrappedHandleContainerKeyDown, onClear: handleClear, inputRef: handleRef, // Additional enableAccessibleFieldDOMStructure: false, placeholder, inputMode, autoComplete: 'off', value: shouldShowPlaceholder ? '' : valueStr, onChange: handleInputChange, focused, disabled, readOnly, autoFocus, openPickerAriaLabel }); }; exports.useFieldV6TextField = useFieldV6TextField; function isFieldFocused(inputRef) { return inputRef.current === (0, _utils.getActiveElement)(document); }