@mui/x-date-pickers
Version:
The community edition of the Date and Time Picker components (MUI X).
350 lines (341 loc) • 15.6 kB
JavaScript
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 _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback"));
var _useForkRef = _interopRequireDefault(require("@mui/utils/useForkRef"));
var _utils = require("../../utils/utils");
var _useField = require("./useField.utils");
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 = params => {
const isRtl = (0, _RtlProvider.useRtl)();
const focusTimeoutRef = React.useRef(undefined);
const selectionSyncTimeoutRef = React.useRef(undefined);
const {
forwardedProps: {
onFocus,
onClick,
onPaste,
onBlur,
inputRef: inputRefProp,
placeholder: inPlaceholder
},
internalProps: {
readOnly = false,
disabled = false
},
parsedSelectedSections,
activeSectionIndex,
state,
fieldValueManager,
valueManager,
applyCharacterEditing,
resetCharacterQuery,
updateSectionValue,
updateValueFromValueStr,
clearActiveSection,
clearValue,
setTempAndroidValueStr,
setSelectedSections,
getSectionsFromValue,
areAllSectionsEmpty,
localizedDigits
} = params;
const inputRef = React.useRef(null);
const handleRef = (0, _useForkRef.default)(inputRefProp, inputRef);
const sections = React.useMemo(() => addPositionPropertiesToSections(state.sections, localizedDigits, isRtl), [state.sections, localizedDigits, isRtl]);
const interactions = React.useMemo(() => ({
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);
}
}
clearTimeout(selectionSyncTimeoutRef.current);
selectionSyncTimeoutRef.current = setTimeout(() => {
// 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)) {
interactions.syncSelectionToDOM();
}
});
}
// Even reading this variable seems to do the trick, but also setting it just to make use of it
inputRef.current.scrollTop = currentScrollTop;
},
getActiveSectionIndexFromDOM: () => {
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;
},
focusField: (newSelectedSection = 0) => {
if ((0, _utils.getActiveElement)(document) === inputRef.current) {
return;
}
inputRef.current?.focus();
setSelectedSections(newSelectedSection);
},
setSelectedSections: newSelectedSections => setSelectedSections(newSelectedSections),
isFieldFocused: () => inputRef.current === (0, _utils.getActiveElement)(document)
}), [inputRef, parsedSelectedSections, sections, setSelectedSections]);
const 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);
};
const handleInputFocus = (0, _useEventCallback.default)((...args) => {
onFocus?.(...args);
// The ref is guaranteed to be resolved at this point.
const input = inputRef.current;
clearTimeout(focusTimeoutRef.current);
focusTimeoutRef.current = setTimeout(() => {
// 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) {
resetCharacterQuery();
updateSectionValue({
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;
}
}
resetCharacterQuery();
updateValueFromValueStr(pastedValue);
});
const handleContainerBlur = (0, _useEventCallback.default)((...args) => {
onBlur?.(...args);
setSelectedSections(null);
});
const handleInputChange = (0, _useEventCallback.default)(event => {
if (readOnly) {
return;
}
const targetValue = event.target.value;
if (targetValue === '') {
resetCharacterQuery();
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);
}
resetCharacterQuery();
clearActiveSection();
return;
}
applyCharacterEditing({
keyPressed,
sectionIndex: activeSectionIndex
});
});
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');
}
return () => {
clearTimeout(focusTimeoutRef.current);
clearTimeout(selectionSyncTimeoutRef.current);
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
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;
return {
interactions,
returnedValue: {
// Forwarded
readOnly,
onBlur: handleContainerBlur,
onClick: handleInputClick,
onFocus: handleInputFocus,
onPaste: handleInputPaste,
inputRef: handleRef,
// Additional
enableAccessibleFieldDOMStructure: false,
placeholder,
inputMode,
autoComplete: 'off',
value: shouldShowPlaceholder ? '' : valueStr,
onChange: handleInputChange
}
};
};
exports.useFieldV6TextField = useFieldV6TextField;
;