@mui/x-date-pickers
Version:
The community edition of the Date and Time Picker components (MUI X).
341 lines (334 loc) • 11.3 kB
JavaScript
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
};
};