UNPKG

@mui/x-date-pickers

Version:

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

392 lines (383 loc) 14.4 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import * as React from 'react'; import useControlled from '@mui/utils/useControlled'; import useTimeout from '@mui/utils/useTimeout'; import useEventCallback from '@mui/utils/useEventCallback'; import { useRtl } from '@mui/system/RtlProvider'; import { usePickerTranslations } from "../../../hooks/usePickerTranslations.js"; import { useUtils, useLocalizationContext } from "../useUtils.js"; import { mergeDateIntoReferenceDate, getSectionsBoundaries, validateSections, getDateFromDateSections, parseSelectedSections, getLocalizedDigits, getSectionOrder } from "./useField.utils.js"; import { buildSectionsFromFormat } from "./buildSectionsFromFormat.js"; import { useValidation } from "../../../validation/index.js"; import { useControlledValue } from "../useControlledValue.js"; import { getSectionTypeGranularity } from "../../utils/getDefaultReferenceDate.js"; const QUERY_LIFE_DURATION_MS = 5000; export const useFieldState = parameters => { const utils = useUtils(); const translations = usePickerTranslations(); const adapter = useLocalizationContext(); const isRtl = useRtl(); const { manager: { validator, valueType, internal_valueManager: valueManager, internal_fieldValueManager: fieldValueManager }, internalPropsWithDefaults, internalPropsWithDefaults: { value: valueProp, defaultValue, referenceDate: referenceDateProp, onChange, format, formatDensity = 'dense', selectedSections: selectedSectionsProp, onSelectedSectionsChange, shouldRespectLeadingZeros = false, timezone: timezoneProp, enableAccessibleFieldDOMStructure = true }, forwardedProps: { error: errorProp } } = parameters; const { value, handleValueChange, timezone } = useControlledValue({ name: 'a field component', timezone: timezoneProp, value: valueProp, defaultValue, referenceDate: referenceDateProp, onChange, valueManager }); const valueRef = React.useRef(value); React.useEffect(() => { valueRef.current = value; }, [value]); const { hasValidationError } = useValidation({ props: internalPropsWithDefaults, validator, timezone, value, onError: internalPropsWithDefaults.onError }); const error = React.useMemo(() => { // only override when `error` is undefined. // in case of multi input fields, the `error` value is provided externally and will always be defined. if (errorProp !== undefined) { return errorProp; } return hasValidationError; }, [hasValidationError, errorProp]); const localizedDigits = React.useMemo(() => getLocalizedDigits(utils), [utils]); const sectionsValueBoundaries = React.useMemo(() => getSectionsBoundaries(utils, localizedDigits, timezone), [utils, localizedDigits, timezone]); const getSectionsFromValue = React.useCallback(valueToAnalyze => fieldValueManager.getSectionsFromValue(valueToAnalyze, date => buildSectionsFromFormat({ utils, localeText: translations, localizedDigits, format, date, formatDensity, shouldRespectLeadingZeros, enableAccessibleFieldDOMStructure, isRtl })), [fieldValueManager, format, translations, localizedDigits, isRtl, shouldRespectLeadingZeros, utils, formatDensity, enableAccessibleFieldDOMStructure]); const [state, setState] = React.useState(() => { const sections = getSectionsFromValue(value); validateSections(sections, valueType); const stateWithoutReferenceDate = { sections, lastExternalValue: value, lastSectionsDependencies: { format, isRtl, locale: utils.locale }, tempValueStrAndroid: null, characterQuery: null }; const granularity = getSectionTypeGranularity(sections); const referenceValue = valueManager.getInitialReferenceValue({ referenceDate: referenceDateProp, value, utils, props: internalPropsWithDefaults, granularity, timezone }); return _extends({}, stateWithoutReferenceDate, { referenceValue }); }); const [selectedSections, innerSetSelectedSections] = useControlled({ controlled: selectedSectionsProp, default: null, name: 'useField', state: 'selectedSections' }); const setSelectedSections = newSelectedSections => { innerSetSelectedSections(newSelectedSections); onSelectedSectionsChange?.(newSelectedSections); }; const parsedSelectedSections = React.useMemo(() => parseSelectedSections(selectedSections, state.sections), [selectedSections, state.sections]); const activeSectionIndex = parsedSelectedSections === 'all' ? 0 : parsedSelectedSections; const sectionOrder = React.useMemo(() => getSectionOrder(state.sections, isRtl && !enableAccessibleFieldDOMStructure), [state.sections, isRtl, enableAccessibleFieldDOMStructure]); const areAllSectionsEmpty = React.useMemo(() => state.sections.every(section => section.value === ''), [state.sections]); const publishValue = newValue => { const context = { validationError: validator({ adapter, value: newValue, timezone, props: internalPropsWithDefaults }) }; handleValueChange(newValue, context); }; const setSectionValue = (sectionIndex, newSectionValue) => { const newSections = [...state.sections]; newSections[sectionIndex] = _extends({}, newSections[sectionIndex], { value: newSectionValue, modified: true }); return newSections; }; const sectionToUpdateOnNextInvalidDateRef = React.useRef(null); const updateSectionValueOnNextInvalidDateTimeout = useTimeout(); const setSectionUpdateToApplyOnNextInvalidDate = newSectionValue => { if (activeSectionIndex == null) { return; } sectionToUpdateOnNextInvalidDateRef.current = { sectionIndex: activeSectionIndex, value: newSectionValue }; updateSectionValueOnNextInvalidDateTimeout.start(0, () => { sectionToUpdateOnNextInvalidDateRef.current = null; }); }; const clearValue = () => { if (valueManager.areValuesEqual(utils, value, valueManager.emptyValue)) { setState(prevState => _extends({}, prevState, { sections: prevState.sections.map(section => _extends({}, section, { value: '' })), tempValueStrAndroid: null, characterQuery: null })); } else { setState(prevState => _extends({}, prevState, { characterQuery: null })); publishValue(valueManager.emptyValue); } }; const clearActiveSection = () => { if (activeSectionIndex == null) { return; } const activeSection = state.sections[activeSectionIndex]; if (activeSection.value === '') { return; } setSectionUpdateToApplyOnNextInvalidDate(''); if (fieldValueManager.getDateFromSection(value, activeSection) === null) { setState(prevState => _extends({}, prevState, { sections: setSectionValue(activeSectionIndex, ''), tempValueStrAndroid: null, characterQuery: null })); } else { setState(prevState => _extends({}, prevState, { characterQuery: null })); publishValue(fieldValueManager.updateDateInValue(value, activeSection, null)); } }; const updateValueFromValueStr = valueStr => { const parseDateStr = (dateStr, referenceDate) => { const date = utils.parse(dateStr, format); if (!utils.isValid(date)) { return null; } const sections = buildSectionsFromFormat({ utils, localeText: translations, localizedDigits, format, date, formatDensity, shouldRespectLeadingZeros, enableAccessibleFieldDOMStructure, isRtl }); return mergeDateIntoReferenceDate(utils, date, sections, referenceDate, false); }; const newValue = fieldValueManager.parseValueStr(valueStr, state.referenceValue, parseDateStr); publishValue(newValue); }; const cleanActiveDateSectionsIfValueNullTimeout = useTimeout(); const updateSectionValue = ({ section, newSectionValue, shouldGoToNextSection }) => { updateSectionValueOnNextInvalidDateTimeout.clear(); cleanActiveDateSectionsIfValueNullTimeout.clear(); const activeDate = fieldValueManager.getDateFromSection(value, section); /** * Decide which section should be focused */ if (shouldGoToNextSection && activeSectionIndex < state.sections.length - 1) { setSelectedSections(activeSectionIndex + 1); } /** * Try to build a valid date from the new section value */ const newSections = setSectionValue(activeSectionIndex, newSectionValue); const newActiveDateSections = fieldValueManager.getDateSectionsFromValue(newSections, section); const newActiveDate = getDateFromDateSections(utils, newActiveDateSections, localizedDigits); /** * If the new date is valid, * Then we merge the value of the modified sections into the reference date. * This makes sure that we don't lose some information of the initial date (like the time on a date field). */ if (utils.isValid(newActiveDate)) { const mergedDate = mergeDateIntoReferenceDate(utils, newActiveDate, newActiveDateSections, fieldValueManager.getDateFromSection(state.referenceValue, section), true); if (activeDate == null) { cleanActiveDateSectionsIfValueNullTimeout.start(0, () => { if (valueRef.current === value) { setState(prevState => _extends({}, prevState, { sections: fieldValueManager.clearDateSections(state.sections, section), tempValueStrAndroid: null })); } }); } return publishValue(fieldValueManager.updateDateInValue(value, section, mergedDate)); } /** * If all the sections are filled but the date is invalid, * Then we publish an invalid date. */ if (newActiveDateSections.every(sectionBis => sectionBis.value !== '')) { setSectionUpdateToApplyOnNextInvalidDate(newSectionValue); return publishValue(fieldValueManager.updateDateInValue(value, section, newActiveDate)); } /** * If the previous date is not null, * Then we publish the date as `null`. */ if (activeDate != null) { setSectionUpdateToApplyOnNextInvalidDate(newSectionValue); return publishValue(fieldValueManager.updateDateInValue(value, section, null)); } /** * If the previous date is already null, * Then we don't publish the date and we update the sections. */ return setState(prevState => _extends({}, prevState, { sections: newSections, tempValueStrAndroid: null })); }; const setTempAndroidValueStr = tempValueStrAndroid => setState(prevState => _extends({}, prevState, { tempValueStrAndroid })); const setCharacterQuery = useEventCallback(newCharacterQuery => { setState(prevState => _extends({}, prevState, { characterQuery: newCharacterQuery })); }); // If `prop.value` changes, we update the state to reflect the new value if (value !== state.lastExternalValue) { let sections; if (sectionToUpdateOnNextInvalidDateRef.current != null && !utils.isValid(fieldValueManager.getDateFromSection(value, state.sections[sectionToUpdateOnNextInvalidDateRef.current.sectionIndex]))) { sections = setSectionValue(sectionToUpdateOnNextInvalidDateRef.current.sectionIndex, sectionToUpdateOnNextInvalidDateRef.current.value); } else { sections = getSectionsFromValue(value); } setState(prevState => _extends({}, prevState, { lastExternalValue: value, sections, sectionsDependencies: { format, isRtl, locale: utils.locale }, referenceValue: fieldValueManager.updateReferenceValue(utils, value, prevState.referenceValue), tempValueStrAndroid: null })); } if (isRtl !== state.lastSectionsDependencies.isRtl || format !== state.lastSectionsDependencies.format || utils.locale !== state.lastSectionsDependencies.locale) { const sections = getSectionsFromValue(value); validateSections(sections, valueType); setState(prevState => _extends({}, prevState, { lastSectionsDependencies: { format, isRtl, locale: utils.locale }, sections, tempValueStrAndroid: null, characterQuery: null })); } if (state.characterQuery != null && !error && activeSectionIndex == null) { setCharacterQuery(null); } if (state.characterQuery != null && state.sections[state.characterQuery.sectionIndex]?.type !== state.characterQuery.sectionType) { setCharacterQuery(null); } React.useEffect(() => { if (sectionToUpdateOnNextInvalidDateRef.current != null) { sectionToUpdateOnNextInvalidDateRef.current = null; } }); const cleanCharacterQueryTimeout = useTimeout(); React.useEffect(() => { if (state.characterQuery != null) { cleanCharacterQueryTimeout.start(QUERY_LIFE_DURATION_MS, () => setCharacterQuery(null)); } return () => {}; }, [state.characterQuery, setCharacterQuery, cleanCharacterQueryTimeout]); // If `tempValueStrAndroid` is still defined for some section when running `useEffect`, // Then `onChange` has only been called once, which means the user pressed `Backspace` to reset the section. // This causes a small flickering on Android, // But we can't use `useEnhancedEffect` which is always called before the second `onChange` call and then would cause false positives. React.useEffect(() => { if (state.tempValueStrAndroid != null && activeSectionIndex != null) { clearActiveSection(); } }, [state.sections]); // eslint-disable-line react-hooks/exhaustive-deps return { // States and derived states activeSectionIndex, areAllSectionsEmpty, error, localizedDigits, parsedSelectedSections, sectionOrder, sectionsValueBoundaries, state, timezone, value, // Methods to update the states clearValue, clearActiveSection, setCharacterQuery, setSelectedSections, setTempAndroidValueStr, updateSectionValue, updateValueFromValueStr, // Utilities methods getSectionsFromValue }; };