355 lines
12 KiB
JavaScript
355 lines
12 KiB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
|
|
import * as React from 'react';
|
|
import { unstable_useControlled as useControlled } from '@mui/utils';
|
|
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',
|
|
selectedSections: selectedSectionsProp,
|
|
onSelectedSectionsChange,
|
|
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: 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: 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 [selectedSections, setSelectedSections] = useControlled({
|
|
controlled: selectedSectionsProp,
|
|
default: null,
|
|
name: 'usePickerValue',
|
|
state: 'selectedSections'
|
|
});
|
|
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
|
|
};
|
|
|
|
// TODO v7: Remove 2nd condition
|
|
if (action.name === 'setValueFromShortcut' && action.shortcut != null) {
|
|
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(() => setIsOpen(true));
|
|
const handleClose = useEventCallback(() => setIsOpen(false));
|
|
const handleChange = useEventCallback((newValue, selectionState = 'partial') => updateDate({
|
|
name: 'setValueFromView',
|
|
value: newValue,
|
|
selectionState
|
|
}));
|
|
|
|
// TODO v7: Make changeImportance and label mandatory.
|
|
const handleSelectShortcut = useEventCallback((newValue, changeImportance, shortcut) => updateDate({
|
|
name: 'setValueFromShortcut',
|
|
value: newValue,
|
|
changeImportance: changeImportance != null ? changeImportance : 'accept',
|
|
shortcut
|
|
}));
|
|
const handleChangeFromField = useEventCallback((newValue, context) => updateDate({
|
|
name: 'setValueFromField',
|
|
value: newValue,
|
|
context
|
|
}));
|
|
const handleFieldSelectedSectionsChange = useEventCallback(newSelectedSections => {
|
|
setSelectedSections(newSelectedSections);
|
|
onSelectedSectionsChange == null || onSelectedSectionsChange(newSelectedSections);
|
|
});
|
|
const actions = {
|
|
onClear: handleClear,
|
|
onAccept: handleAccept,
|
|
onDismiss: handleDismiss,
|
|
onCancel: handleCancel,
|
|
onSetToday: handleSetToday,
|
|
onOpen: handleOpen,
|
|
onClose: handleClose
|
|
};
|
|
const fieldResponse = {
|
|
value: dateState.draft,
|
|
onChange: handleChangeFromField,
|
|
selectedSections,
|
|
onSelectedSectionsChange: handleFieldSelectedSectionsChange
|
|
};
|
|
const viewValue = React.useMemo(() => valueManager.cleanValue(utils, dateState.draft), [utils, valueManager, dateState.draft]);
|
|
const viewResponse = {
|
|
value: viewValue,
|
|
onChange: handleChange,
|
|
onClose: handleClose,
|
|
open: isOpen,
|
|
onSelectedSectionsChange: handleFieldSelectedSectionsChange
|
|
};
|
|
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
|
|
};
|
|
}; |