UNPKG

@mui/x-date-pickers

Version:

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

341 lines (334 loc) 11.3 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { useOpenState } from '../useOpenState'; import { useLocalizationContext, useUtils } from '../useUtils'; import { useValidation } from '../useValidation'; import { useValueWithTimezone } from '../useValueWithTimezone'; /** * Decide if the new value should be published * The published value will be passed to `onChange` if defined. */ const shouldPublishValue = params => { const { action, hasChanged, dateState, isControlled } = params; const isCurrentValueTheDefaultValue = !isControlled && !dateState.hasBeenModifiedSinceMount; // The field is responsible for only calling `onChange` when needed. if (action.name === 'setValueFromField') { return true; } if (action.name === 'setValueFromAction') { // If the component is not controlled, and the value has not been modified since the mount, // Then we want to publish the default value whenever the user pressed the "Accept", "Today" or "Clear" button. if (isCurrentValueTheDefaultValue && ['accept', 'today', 'clear'].includes(action.pickerAction)) { return true; } return hasChanged(dateState.lastPublishedValue); } if (action.name === 'setValueFromView' && action.selectionState !== 'shallow') { // On the first view, // If the value is not controlled, then clicking on any value (including the one equal to `defaultValue`) should call `onChange` if (isCurrentValueTheDefaultValue) { return true; } return hasChanged(dateState.lastPublishedValue); } if (action.name === 'setValueFromShortcut') { // On the first view, // If the value is not controlled, then clicking on any value (including the one equal to `defaultValue`) should call `onChange` if (isCurrentValueTheDefaultValue) { return true; } return hasChanged(dateState.lastPublishedValue); } return false; }; /** * Decide if the new value should be committed. * The committed value will be passed to `onAccept` if defined. * It will also be used as a reset target when calling the `cancel` picker action (when clicking on the "Cancel" button). */ const shouldCommitValue = params => { const { action, hasChanged, dateState, isControlled, closeOnSelect } = params; const isCurrentValueTheDefaultValue = !isControlled && !dateState.hasBeenModifiedSinceMount; if (action.name === 'setValueFromAction') { // If the component is not controlled, and the value has not been modified since the mount, // Then we want to commit the default value whenever the user pressed the "Accept", "Today" or "Clear" button. if (isCurrentValueTheDefaultValue && ['accept', 'today', 'clear'].includes(action.pickerAction)) { return true; } return hasChanged(dateState.lastCommittedValue); } if (action.name === 'setValueFromView' && action.selectionState === 'finish' && closeOnSelect) { // On picker where the 1st view is also the last view, // If the value is not controlled, then clicking on any value (including the one equal to `defaultValue`) should call `onAccept` if (isCurrentValueTheDefaultValue) { return true; } return hasChanged(dateState.lastCommittedValue); } if (action.name === 'setValueFromShortcut') { return action.changeImportance === 'accept' && hasChanged(dateState.lastCommittedValue); } return false; }; /** * Decide if the picker should be closed after the value is updated. */ const shouldClosePicker = params => { const { action, closeOnSelect } = params; if (action.name === 'setValueFromAction') { return true; } if (action.name === 'setValueFromView') { return action.selectionState === 'finish' && closeOnSelect; } if (action.name === 'setValueFromShortcut') { return action.changeImportance === 'accept'; } return false; }; /** * Manage the value lifecycle of all the pickers. */ export const usePickerValue = ({ props, valueManager, valueType, wrapperVariant, validator }) => { const { onAccept, onChange, value: inValue, defaultValue: inDefaultValue, closeOnSelect = wrapperVariant === 'desktop', timezone: timezoneProp } = props; const { current: defaultValue } = React.useRef(inDefaultValue); const { current: isControlled } = React.useRef(inValue !== undefined); /* eslint-disable react-hooks/rules-of-hooks, react-hooks/exhaustive-deps */ if (process.env.NODE_ENV !== 'production') { React.useEffect(() => { if (isControlled !== (inValue !== undefined)) { console.error([`MUI X: A component is changing the ${isControlled ? '' : 'un'}controlled value of a picker to be ${isControlled ? 'un' : ''}controlled.`, 'Elements should not switch from uncontrolled to controlled (or vice versa).', `Decide between using a controlled or uncontrolled value` + 'for the lifetime of the component.', "The nature of the state is determined during the first render. It's considered controlled if the value is not `undefined`.", 'More info: https://fb.me/react-controlled-components'].join('\n')); } }, [inValue]); React.useEffect(() => { if (!isControlled && defaultValue !== inDefaultValue) { console.error([`MUI X: A component is changing the defaultValue of an uncontrolled picker after being initialized. ` + `To suppress this warning opt to use a controlled value.`].join('\n')); } }, [JSON.stringify(defaultValue)]); } /* eslint-enable react-hooks/rules-of-hooks, react-hooks/exhaustive-deps */ const utils = useUtils(); const adapter = useLocalizationContext(); const { isOpen, setIsOpen } = useOpenState(props); const [dateState, setDateState] = React.useState(() => { let initialValue; if (inValue !== undefined) { initialValue = inValue; } else if (defaultValue !== undefined) { initialValue = defaultValue; } else { initialValue = valueManager.emptyValue; } return { draft: initialValue, lastPublishedValue: initialValue, lastCommittedValue: initialValue, lastControlledValue: inValue, hasBeenModifiedSinceMount: false }; }); const { timezone, handleValueChange } = useValueWithTimezone({ timezone: timezoneProp, value: inValue, defaultValue, onChange, valueManager }); useValidation(_extends({}, props, { value: dateState.draft, timezone }), validator, valueManager.isSameError, valueManager.defaultErrorState); const updateDate = useEventCallback(action => { const updaterParams = { action, dateState, hasChanged: comparison => !valueManager.areValuesEqual(utils, action.value, comparison), isControlled, closeOnSelect }; const shouldPublish = shouldPublishValue(updaterParams); const shouldCommit = shouldCommitValue(updaterParams); const shouldClose = shouldClosePicker(updaterParams); setDateState(prev => _extends({}, prev, { draft: action.value, lastPublishedValue: shouldPublish ? action.value : prev.lastPublishedValue, lastCommittedValue: shouldCommit ? action.value : prev.lastCommittedValue, hasBeenModifiedSinceMount: true })); if (shouldPublish) { const validationError = action.name === 'setValueFromField' ? action.context.validationError : validator({ adapter, value: action.value, props: _extends({}, props, { value: action.value, timezone }) }); const context = { validationError }; if (action.name === 'setValueFromShortcut') { context.shortcut = action.shortcut; } handleValueChange(action.value, context); } if (shouldCommit && onAccept) { onAccept(action.value); } if (shouldClose) { setIsOpen(false); } }); if (inValue !== undefined && (dateState.lastControlledValue === undefined || !valueManager.areValuesEqual(utils, dateState.lastControlledValue, inValue))) { const isUpdateComingFromPicker = valueManager.areValuesEqual(utils, dateState.draft, inValue); setDateState(prev => _extends({}, prev, { lastControlledValue: inValue }, isUpdateComingFromPicker ? {} : { lastCommittedValue: inValue, lastPublishedValue: inValue, draft: inValue, hasBeenModifiedSinceMount: true })); } const handleClear = useEventCallback(() => { updateDate({ value: valueManager.emptyValue, name: 'setValueFromAction', pickerAction: 'clear' }); }); const handleAccept = useEventCallback(() => { updateDate({ value: dateState.lastPublishedValue, name: 'setValueFromAction', pickerAction: 'accept' }); }); const handleDismiss = useEventCallback(() => { updateDate({ value: dateState.lastPublishedValue, name: 'setValueFromAction', pickerAction: 'dismiss' }); }); const handleCancel = useEventCallback(() => { updateDate({ value: dateState.lastCommittedValue, name: 'setValueFromAction', pickerAction: 'cancel' }); }); const handleSetToday = useEventCallback(() => { updateDate({ value: valueManager.getTodayValue(utils, timezone, valueType), name: 'setValueFromAction', pickerAction: 'today' }); }); const handleOpen = useEventCallback(event => { event.preventDefault(); setIsOpen(true); }); const handleClose = useEventCallback(event => { event?.preventDefault(); setIsOpen(false); }); const handleChange = useEventCallback((newValue, selectionState = 'partial') => updateDate({ name: 'setValueFromView', value: newValue, selectionState })); const handleSelectShortcut = useEventCallback((newValue, changeImportance, shortcut) => updateDate({ name: 'setValueFromShortcut', value: newValue, changeImportance, shortcut })); const handleChangeFromField = useEventCallback((newValue, context) => updateDate({ name: 'setValueFromField', value: newValue, context })); const actions = { onClear: handleClear, onAccept: handleAccept, onDismiss: handleDismiss, onCancel: handleCancel, onSetToday: handleSetToday, onOpen: handleOpen, onClose: handleClose }; const fieldResponse = { value: dateState.draft, onChange: handleChangeFromField }; const viewValue = React.useMemo(() => valueManager.cleanValue(utils, dateState.draft), [utils, valueManager, dateState.draft]); const viewResponse = { value: viewValue, onChange: handleChange, onClose: handleClose, open: isOpen }; const isValid = testedValue => { const error = validator({ adapter, value: testedValue, props: _extends({}, props, { value: testedValue, timezone }) }); return !valueManager.hasError(error); }; const layoutResponse = _extends({}, actions, { value: viewValue, onChange: handleChange, onSelectShortcut: handleSelectShortcut, isValid }); return { open: isOpen, fieldProps: fieldResponse, viewProps: viewResponse, layoutProps: layoutResponse, actions }; };