@mui/x-date-pickers
Version:
The community edition of the MUI X Date and Time Picker components.
323 lines (316 loc) • 11.5 kB
JavaScript
import useEventCallback from '@mui/utils/useEventCallback';
import useTimeout from '@mui/utils/useTimeout';
import { getActiveElement } from "../../utils/utils.mjs";
import { syncSelectionToDOM } from "./syncSelectionToDOM.mjs";
import { usePickerAdapter } from "../../../hooks/usePickerAdapter.mjs";
import { cleanDigitSectionValue, getLetterEditingOptions, removeLocalizedDigits } from "./useField.utils.mjs";
/**
* Generate the props to pass to the root element of the field.
* @param {UseFieldRootPropsParameters} parameters The parameters of the hook.
* @returns {UseFieldRootPropsReturnValue} The props to forward to the root element of the field.
*/
export function useFieldRootProps(parameters) {
const {
manager: {
internal_fieldValueManager: fieldValueManager
},
focused,
setFocused,
domGetters,
stateResponse,
applyCharacterEditing,
internalPropsWithDefaults,
stateResponse: {
// States and derived states
parsedSelectedSections,
sectionsValueBoundaries,
sectionOrder,
state,
value,
activeSectionIndex,
localizedDigits,
timezone,
// Methods to update the states
clearValue,
clearActiveSection,
setCharacterQuery,
setSelectedSections,
updateValueFromValueStr,
updateSectionValue
},
internalPropsWithDefaults: {
disabled = false,
readOnly = false,
minutesStep
}
} = parameters;
const adapter = usePickerAdapter();
const handleKeyDown = useEventCallback(event => {
if (disabled) {
return;
}
// eslint-disable-next-line default-case
switch (true) {
// Select all
case (event.ctrlKey || event.metaKey) && String.fromCharCode(event.keyCode) === 'A' && !event.shiftKey && !event.altKey:
{
// prevent default to make sure that the next line "select all" while updating
// the internal state at the same time.
event.preventDefault();
setSelectedSections('all');
break;
}
// Move selection to next section
case event.key === 'ArrowRight':
{
event.preventDefault();
if (parsedSelectedSections == null) {
setSelectedSections(sectionOrder.startIndex);
} else if (parsedSelectedSections === 'all') {
setSelectedSections(sectionOrder.endIndex);
} else {
const nextSectionIndex = sectionOrder.neighbors[parsedSelectedSections].rightIndex;
if (nextSectionIndex !== null) {
setSelectedSections(nextSectionIndex);
}
}
break;
}
// Move selection to previous section
case event.key === 'ArrowLeft':
{
event.preventDefault();
if (parsedSelectedSections == null) {
setSelectedSections(sectionOrder.endIndex);
} else if (parsedSelectedSections === 'all') {
setSelectedSections(sectionOrder.startIndex);
} else {
const nextSectionIndex = sectionOrder.neighbors[parsedSelectedSections].leftIndex;
if (nextSectionIndex !== null) {
setSelectedSections(nextSectionIndex);
}
}
break;
}
// Reset the value of the selected section
case event.key === 'Delete':
{
event.preventDefault();
if (readOnly) {
break;
}
if (parsedSelectedSections == null || parsedSelectedSections === 'all') {
clearValue();
} else {
clearActiveSection();
}
break;
}
// Increment / decrement the selected section value
case ['ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key):
{
event.preventDefault();
if (readOnly || activeSectionIndex == null) {
break;
}
// if all sections are selected, mark the currently editing one as selected
if (parsedSelectedSections === 'all') {
setSelectedSections(activeSectionIndex);
}
const activeSection = state.sections[activeSectionIndex];
const newSectionValue = adjustSectionValue(adapter, timezone, activeSection, event.key, sectionsValueBoundaries, localizedDigits, fieldValueManager.getDateFromSection(value, activeSection), {
minutesStep
});
updateSectionValue({
section: activeSection,
newSectionValue,
shouldGoToNextSection: false
});
break;
}
}
});
const containerClickTimeout = useTimeout();
const handleClick = useEventCallback(event => {
if (disabled || !domGetters.isReady()) {
return;
}
setFocused(true);
if (parsedSelectedSections === 'all') {
containerClickTimeout.start(0, () => {
const cursorPosition = document.getSelection().getRangeAt(0).startOffset;
if (cursorPosition === 0) {
setSelectedSections(sectionOrder.startIndex);
return;
}
let sectionIndex = 0;
let cursorOnStartOfSection = 0;
while (cursorOnStartOfSection < cursorPosition && sectionIndex < state.sections.length) {
const section = state.sections[sectionIndex];
sectionIndex += 1;
cursorOnStartOfSection += `${section.startSeparator}${section.value || section.placeholder}${section.endSeparator}`.length;
}
setSelectedSections(sectionIndex - 1);
});
} else if (!focused) {
setFocused(true);
setSelectedSections(sectionOrder.startIndex);
} else {
const hasClickedOnASection = domGetters.getRoot().contains(event.target);
if (!hasClickedOnASection) {
setSelectedSections(sectionOrder.startIndex);
}
}
});
const handleInput = useEventCallback(event => {
if (!domGetters.isReady() || parsedSelectedSections !== 'all') {
return;
}
const target = event.target;
const keyPressed = target.textContent ?? '';
domGetters.getRoot().innerHTML = state.sections.map(section => `${section.startSeparator}${section.value || section.placeholder}${section.endSeparator}`).join('');
syncSelectionToDOM({
focused,
domGetters,
stateResponse
});
if (keyPressed.length === 0 || keyPressed.charCodeAt(0) === 10) {
clearValue();
setSelectedSections('all');
} else if (keyPressed.length > 1) {
updateValueFromValueStr(keyPressed);
} else {
if (parsedSelectedSections === 'all') {
setSelectedSections(0);
}
applyCharacterEditing({
keyPressed,
sectionIndex: 0
});
}
});
const handlePaste = useEventCallback(event => {
if (readOnly || parsedSelectedSections !== 'all') {
event.preventDefault();
return;
}
const pastedValue = event.clipboardData.getData('text');
event.preventDefault();
setCharacterQuery(null);
updateValueFromValueStr(pastedValue);
});
const handleFocus = useEventCallback(() => {
if (focused || disabled || !domGetters.isReady()) {
return;
}
const activeElement = getActiveElement(domGetters.getRoot());
setFocused(true);
const isFocusInsideASection = domGetters.getSectionIndexFromDOMElement(activeElement) != null;
if (!isFocusInsideASection) {
setSelectedSections(sectionOrder.startIndex);
}
});
const handleBlur = useEventCallback(() => {
setTimeout(() => {
if (!domGetters.isReady()) {
return;
}
const activeElement = getActiveElement(domGetters.getRoot());
const shouldBlur = !domGetters.getRoot().contains(activeElement);
if (shouldBlur) {
setFocused(false);
setSelectedSections(null);
}
});
});
return {
// Event handlers
onKeyDown: handleKeyDown,
onBlur: handleBlur,
onFocus: handleFocus,
onClick: handleClick,
onPaste: handlePaste,
onInput: handleInput,
// Other
contentEditable: parsedSelectedSections === 'all',
tabIndex: internalPropsWithDefaults.disabled || parsedSelectedSections === 0 ? -1 : 0 // TODO: Try to set to undefined when there is a section selected.
};
}
function getDeltaFromKeyCode(keyCode) {
switch (keyCode) {
case 'ArrowUp':
return 1;
case 'ArrowDown':
return -1;
case 'PageUp':
return 5;
case 'PageDown':
return -5;
default:
return 0;
}
}
function adjustSectionValue(adapter, timezone, section, keyCode, sectionsValueBoundaries, localizedDigits, activeDate, stepsAttributes) {
const delta = getDeltaFromKeyCode(keyCode);
const isStart = keyCode === 'Home';
const isEnd = keyCode === 'End';
const shouldSetAbsolute = section.value === '' || isStart || isEnd;
const adjustDigitSection = () => {
const sectionBoundaries = sectionsValueBoundaries[section.type]({
currentDate: activeDate,
format: section.format,
contentType: section.contentType
});
const getCleanValue = value => cleanDigitSectionValue(adapter, value, sectionBoundaries, localizedDigits, section);
const step = section.type === 'minutes' && stepsAttributes?.minutesStep ? stepsAttributes.minutesStep : 1;
let newSectionValueNumber;
if (shouldSetAbsolute) {
if (section.type === 'year' && !isEnd && !isStart) {
return adapter.formatByString(adapter.date(undefined, timezone), section.format);
}
if (delta > 0 || isStart) {
newSectionValueNumber = sectionBoundaries.minimum;
} else {
newSectionValueNumber = sectionBoundaries.maximum;
}
} else {
const currentSectionValue = parseInt(removeLocalizedDigits(section.value, localizedDigits), 10);
newSectionValueNumber = currentSectionValue + delta * step;
}
if (newSectionValueNumber % step !== 0) {
if (delta < 0 || isStart) {
newSectionValueNumber += step - (step + newSectionValueNumber) % step; // for JS -3 % 5 = -3 (should be 2)
}
if (delta > 0 || isEnd) {
newSectionValueNumber -= newSectionValueNumber % step;
}
}
if (newSectionValueNumber > sectionBoundaries.maximum) {
return getCleanValue(sectionBoundaries.minimum + (newSectionValueNumber - sectionBoundaries.maximum - 1) % (sectionBoundaries.maximum - sectionBoundaries.minimum + 1));
}
if (newSectionValueNumber < sectionBoundaries.minimum) {
return getCleanValue(sectionBoundaries.maximum - (sectionBoundaries.minimum - newSectionValueNumber - 1) % (sectionBoundaries.maximum - sectionBoundaries.minimum + 1));
}
return getCleanValue(newSectionValueNumber);
};
const adjustLetterSection = () => {
const options = getLetterEditingOptions(adapter, timezone, section.type, section.format);
if (options.length === 0) {
return section.value;
}
if (shouldSetAbsolute) {
if (delta > 0 || isStart) {
return options[0];
}
return options[options.length - 1];
}
const currentOptionIndex = options.indexOf(section.value);
const newOptionIndex = (currentOptionIndex + delta) % options.length;
const clampedIndex = (newOptionIndex + options.length) % options.length;
return options[clampedIndex];
};
if (section.contentType === 'digit' || section.contentType === 'digit-with-letter') {
return adjustDigitSection();
}
return adjustLetterSection();
}