UNPKG

@mui/x-date-pickers

Version:

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

416 lines (406 loc) 16 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.useFieldRootProps = useFieldRootProps; var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallback")); var _useTimeout = _interopRequireDefault(require("@mui/utils/useTimeout")); var _utils = require("../../utils/utils"); var _syncSelectionToDOM = require("./syncSelectionToDOM"); var _usePickerAdapter = require("../../../hooks/usePickerAdapter"); var _useField = require("./useField.utils"); /** * 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. */ 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 = (0, _usePickerAdapter.usePickerAdapter)(); const handleKeyDown = (0, _useEventCallback.default)(event => { if (disabled) { return; } // eslint-disable-next-line default-case switch (true) { // Select all case (event.ctrlKey || event.metaKey) && (event.key?.toUpperCase() === 'A' || 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 = (0, _useTimeout.default)(); const handleClick = (0, _useEventCallback.default)(() => { if (disabled || !domGetters.isReady()) { return; } if (parsedSelectedSections === 'all') { setFocused(true); containerClickTimeout.start(0, () => { // `getSelection`/`getRangeAt` can be unset transiently (e.g. focus // moved before this 0-tick callback ran). Fall back to the first // section in that case. const selection = document.getSelection(); if (!selection || selection.rangeCount === 0) { setSelectedSections(sectionOrder.startIndex); return; } const cursorPosition = selection.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); }); return; } // For trusted pointer input, `handleMouseDown` already ran and set the // section. This branch only matters for `click` events that arrive // without a preceding `mousedown` (programmatic `element.click()`, some // assistive-technology activations, synthetic event sequences in tests). // Fall back to the first section so the field doesn't enter a "focused // but no active section" state. if (!focused) { setFocused(true); if (parsedSelectedSections == null) { setSelectedSections(sectionOrder.startIndex); } } }); // Replaces Chromium's focus delegation with an explicit section focus on // every primary mousedown inside the sections container. The CSS gate // (`WebkitUserModify: read-only` while not `:focus-within`) can make the // section's contenteditable span temporarily non-focusable, in which case // Chromium falls back to the sections-container `tabindex=0` and our // `handleFocus` then runs the "no active section" fallback, briefly // selecting the first section before the click bubble corrects it. // Setting the target section here (and letting `syncSelectionToDOM` move // focus via `.focus()`, which works even with the user-modify rule // active) skips that race entirely. const handleMouseDown = (0, _useEventCallback.default)(event => { if (disabled || !domGetters.isReady() || parsedSelectedSections === 'all') { return; } if (event.button !== 0) { return; } const target = event.target; // `sectionListRoot` is the sections container (a descendant of the // InputBase root that owns this handler). The guard rejects clicks on // sibling adornments (open / clear buttons, etc.) which have their own // behavior and must not get intercepted here. const sectionListRoot = domGetters.getRoot(); if (!sectionListRoot.contains(target)) { return; } // Prefer the visually-containing section (matches Chromium's // delegation + section container `onClick`), fall back to the // closest-by-distance section for padding / past-last-section clicks. const sectionElement = target.closest('[data-sectionindex]'); const parsedIndex = sectionElement ? Number(sectionElement.dataset.sectionindex) : findClosestSectionIndexToPoint(sectionListRoot, event.clientX); // `Number(undefined) === NaN` and `NaN == null === false`, so guard // explicitly here even though `data-sectionindex` is set by // `useFieldSectionContainerProps` for every section in practice. if (parsedIndex == null || !Number.isInteger(parsedIndex)) { return; } event.preventDefault(); setFocused(true); // `mousedown` is now authoritative for pointer section selection. The // section container's own `onClick` deduplicates against the resulting // `parsedSelectedSections` on the click bubble (see // `useFieldSectionContainerProps`), so we always select here and let that // guard absorb the redundant call -- matching the pre-PR // `onSelectedSectionsChange` invocation count. setSelectedSections(parsedIndex); }); const handleInput = (0, _useEventCallback.default)(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(''); (0, _syncSelectionToDOM.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 = (0, _useEventCallback.default)(event => { if (readOnly || parsedSelectedSections !== 'all') { event.preventDefault(); return; } const pastedValue = event.clipboardData.getData('text'); event.preventDefault(); setCharacterQuery(null); updateValueFromValueStr(pastedValue); }); const handleFocus = (0, _useEventCallback.default)(() => { if (focused || disabled || !domGetters.isReady()) { return; } const activeElement = (0, _utils.getActiveElement)(domGetters.getRoot()); setFocused(true); const isFocusInsideASection = domGetters.getSectionIndexFromDOMElement(activeElement) != null; if (!isFocusInsideASection) { setSelectedSections(sectionOrder.startIndex); } }); const handleBlur = (0, _useEventCallback.default)(() => { setTimeout(() => { if (!domGetters.isReady()) { return; } const activeElement = (0, _utils.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, onMouseDown: handleMouseDown, 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. }; } /** * Returns the index of the section whose horizontal center is closest to `clientX`. * Returns `null` if the field renders no `[role="spinbutton"]` descendants * (defensive — every section content span sets `role="spinbutton"` in practice). */ function findClosestSectionIndexToPoint(root, clientX) { const sections = root.querySelectorAll('[role="spinbutton"]'); if (sections.length === 0) { return null; } let closestIndex = 0; let closestDistance = Infinity; for (let i = 0; i < sections.length; i += 1) { const rect = sections[i].getBoundingClientRect(); const center = (rect.left + rect.right) / 2; const distance = Math.abs(clientX - center); if (distance < closestDistance) { closestDistance = distance; closestIndex = i; } } return closestIndex; } 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 => (0, _useField.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((0, _useField.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 = (0, _useField.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(); }