420 lines
No EOL
18 KiB
JavaScript
420 lines
No EOL
18 KiB
JavaScript
"use strict";
|
|
'use client';
|
|
|
|
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)(inputRef.current) === 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)(inputRef.current)) {
|
|
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)(inputRef.current)) {
|
|
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)(inputRef.current)) {
|
|
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)(inputRef.current) &&
|
|
// 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)(inputRef.current);
|
|
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)(inputRef.current);
|
|
} |