From bfb07869233dc3b8e3a6e09d84769f8143307a27 Mon Sep 17 00:00:00 2001 From: flavien Date: Fri, 20 Dec 2024 10:52:42 +0100 Subject: [PATCH] [pickers] Remove the last props passed to the layout slot --- .../shortcuts/ChangeImportance.tsx | 8 +- .../shortcuts/CustomizedRangeShortcuts.js | 8 +- .../shortcuts/CustomizedRangeShortcuts.tsx | 7 +- .../migration-pickers-v7.md | 133 ++++++++++++++++-- .../DateRangePickerToolbar.tsx | 34 ++--- .../DateTimeRangePickerToolbar.tsx | 34 ++--- .../useDesktopRangePicker.tsx | 8 +- .../hooks/useEnrichedRangePickerFieldProps.ts | 2 +- .../useMobileRangePicker.tsx | 8 +- .../useStaticRangePicker.tsx | 3 +- .../src/internals/utils/date-range-manager.ts | 3 +- .../src/DatePicker/DatePickerToolbar.tsx | 19 +-- .../DateTimePicker/DateTimePickerToolbar.tsx | 76 ++++++---- .../DesktopDateTimePickerLayout.tsx | 4 - .../PickersActionBar.test.tsx | 1 + .../src/PickersLayout/PickersLayout.tsx | 4 - .../src/PickersLayout/PickersLayout.types.ts | 6 +- .../src/PickersLayout/usePickerLayout.tsx | 31 +--- .../src/PickersShortcuts/PickersShortcuts.tsx | 30 ++-- .../src/PickersShortcuts/index.ts | 1 - .../src/TimePicker/TimePickerToolbar.tsx | 46 +++--- packages/x-date-pickers/src/hooks/index.tsx | 1 + .../src/hooks/useIsValidValue.tsx | 12 ++ .../src/hooks/usePickerActionsContext.ts | 16 ++- .../src/hooks/usePickerContext.ts | 10 +- .../internals/components/PickerProvider.tsx | 47 +++++-- .../internals/components/PickersToolbar.tsx | 16 +-- .../internals/hooks/date-helpers-hooks.tsx | 11 +- .../useDesktopPicker/useDesktopPicker.tsx | 3 +- .../hooks/useMobilePicker/useMobilePicker.tsx | 3 +- .../internals/hooks/usePicker/usePicker.ts | 5 - .../hooks/usePicker/usePicker.types.ts | 7 +- .../hooks/usePicker/usePickerProvider.ts | 18 ++- .../hooks/usePicker/usePickerValue.ts | 89 +++++------- .../hooks/usePicker/usePickerValue.types.ts | 78 +++++----- .../hooks/useStaticPicker/useStaticPicker.tsx | 8 +- .../src/internals/models/props/toolbar.ts | 7 +- packages/x-date-pickers/src/models/pickers.ts | 8 ++ .../src/themeAugmentation/props.d.ts | 2 +- scripts/x-date-pickers-pro.exports.json | 3 +- scripts/x-date-pickers.exports.json | 3 +- test/regressions/index.test.ts | 2 +- 42 files changed, 460 insertions(+), 355 deletions(-) create mode 100644 packages/x-date-pickers/src/hooks/useIsValidValue.tsx diff --git a/docs/data/date-pickers/shortcuts/ChangeImportance.tsx b/docs/data/date-pickers/shortcuts/ChangeImportance.tsx index 5ef6c1e18d3e..f494a5358849 100644 --- a/docs/data/date-pickers/shortcuts/ChangeImportance.tsx +++ b/docs/data/date-pickers/shortcuts/ChangeImportance.tsx @@ -4,10 +4,8 @@ import Stack from '@mui/material/Stack'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import { - PickersShortcutsItem, - PickerShortcutChangeImportance, -} from '@mui/x-date-pickers/PickersShortcuts'; +import { PickersShortcutsItem } from '@mui/x-date-pickers/PickersShortcuts'; +import { PickerChangeImportance } from '@mui/x-date-pickers/models'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import ToggleButton from '@mui/material/ToggleButton'; @@ -80,7 +78,7 @@ const shortcutsItems: PickersShortcutsItem[] = [ export default function ChangeImportance() { const [changeImportance, setChangeImportance] = - React.useState('accept'); + React.useState('accept'); return ( diff --git a/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.js b/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.js index 478f2dcb5455..c7aec7387fd8 100644 --- a/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.js +++ b/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.js @@ -9,6 +9,8 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { StaticDateRangePicker } from '@mui/x-date-pickers-pro/StaticDateRangePicker'; +import { useIsValidValue, usePickerContext } from '@mui/x-date-pickers/hooks'; + const shortcutsItems = [ { label: 'This Week', @@ -51,7 +53,9 @@ const shortcutsItems = [ ]; function CustomRangeShortcuts(props) { - const { items, onChange, isValid, changeImportance = 'accept' } = props; + const { items, changeImportance = 'accept' } = props; + const isValid = useIsValidValue(); + const { setValue } = usePickerContext(); if (items == null || items.length === 0) { return null; @@ -63,7 +67,7 @@ function CustomRangeShortcuts(props) { return { label: item.label, onClick: () => { - onChange(newValue, changeImportance, item); + setValue(newValue, { changeImportance, shortcut: item }); }, disabled: !isValid(newValue), }; diff --git a/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.tsx b/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.tsx index 87530990c3a8..3f8d779ac966 100644 --- a/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.tsx +++ b/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.tsx @@ -13,6 +13,7 @@ import { PickersShortcutsProps, } from '@mui/x-date-pickers/PickersShortcuts'; import { DateRange } from '@mui/x-date-pickers-pro/models'; +import { useIsValidValue, usePickerContext } from '@mui/x-date-pickers/hooks'; const shortcutsItems: PickersShortcutsItem>[] = [ { @@ -56,7 +57,9 @@ const shortcutsItems: PickersShortcutsItem>[] = [ ]; function CustomRangeShortcuts(props: PickersShortcutsProps>) { - const { items, onChange, isValid, changeImportance = 'accept' } = props; + const { items, changeImportance = 'accept' } = props; + const isValid = useIsValidValue>(); + const { setValue } = usePickerContext>(); if (items == null || items.length === 0) { return null; @@ -68,7 +71,7 @@ function CustomRangeShortcuts(props: PickersShortcutsProps>) { return { label: item.label, onClick: () => { - onChange(newValue, changeImportance, item); + setValue(newValue, { changeImportance, shortcut: item }); }, disabled: !isValid(newValue), }; diff --git a/docs/data/migration/migration-pickers-v7/migration-pickers-v7.md b/docs/data/migration/migration-pickers-v7/migration-pickers-v7.md index d7315974d17d..e57ea74a6ab0 100644 --- a/docs/data/migration/migration-pickers-v7/migration-pickers-v7.md +++ b/docs/data/migration/migration-pickers-v7/migration-pickers-v7.md @@ -364,6 +364,19 @@ This change causes a few breaking changes: ); ``` +- The component passed to the `layout` slot no longer receives the `value` prop, instead you can use the `usePickerContext` hook: + + ```diff + +import { usePickerContext } from '@mui/x-date-pickers/hooks'; + + // This contains a small behavior change. + // If the picker receives an invalid date, + // the old value would equal `null`. + // the new value would equal the invalid date received. + -const { value } = props; + +const { value } = usePickerContext(); + ``` + - The component passed to the `layout` slot no longer receives the `disabled` and `readOnly` props, instead you can use the `usePickerContext` hook: ```diff @@ -376,7 +389,7 @@ This change causes a few breaking changes: +const { readOnly } = usePickerContext(); ``` -- The component passed to the `layout` slot no longer receives an `isRtl` prop. If you need to access this information, you can use the `useRtl` hook from `@mui/system`: +- The component passed to the `layout` slot no longer receives the `isRtl` prop. If you need to access this information, you can use the `useRtl` hook from `@mui/system`: ```diff +import { useRtl } from '@mui/system/RtlProvider'; @@ -398,7 +411,7 @@ This change causes a few breaking changes: +const isLandscape = orientation === 'landscape'; ``` -- The component passed to the `layout` slot no longer receives a `wrapperVariant` prop, instead you can use the `usePickerContext` hook: +- The component passed to the `layout` slot no longer receives the `wrapperVariant` prop, instead you can use the `usePickerContext` hook: ```diff +import { usePickerContext } from '@mui/x-date-pickers/hooks'; @@ -422,7 +435,8 @@ This change causes a few breaking changes: +const { onViewChange } = usePickerContext(); ``` -- The component passed to the `layout` slot no longer receives the `onClear`, `onSetToday`, `onAccept`, `onCancel`, `onOpen`, `onClose` and `onDismiss` props, instead you can use the `usePickerActionsContext` or the `usePickerContext` hooks: +- The component passed to the `layout` slot no longer receives the `onClear`, `onSetToday`, `onAccept`, `onCancel`, `onOpen`, `onClose` `onDismiss`, `onChange` and `onSelectShortcut` props. + You can use the `usePickerActionsContext` or the `usePickerContext` hooks instead: ```diff +import { usePickerActionsContext } from '@mui/x-date-pickers/hooks'; @@ -446,7 +460,7 @@ This change causes a few breaking changes: + setOpen(true); +} - -props.onClose(); + -const { onClose } = props; +const { setOpen } = usePickerActionsContext(); +const onClose = event => { + event.preventDefault(); @@ -460,6 +474,18 @@ This change causes a few breaking changes: -const { onDismiss } = props; +const { acceptValueChanges } = usePickerActionsContext(); +const onDismiss = acceptValueChanges + + -const { onChange } = props; + -onChange(dayjs(), 'partial'); + -onChange(dayjs(), 'finish'); + +const { setValue } = usePickerActionsContext(); + +setValue(dayjs(), { changeImportance: 'set' }); + +setValue(dayjs(), { changeImportance: 'accept' }); + + -const { onSelectShortcut } = props; + -onSelectShortcut(dayjs(), 'accept', myShortcut); + +const { setValue } = usePickerActionsContext(); + +setValue(dayjs(), { changeImportance: 'accept', shortcut: myShortcut }); ``` :::success @@ -469,6 +495,19 @@ This change causes a few breaking changes: ### Slot: `toolbar` +- The component passed to the `toolbar` slot no longer receives the `value` prop, instead you can use the `usePickerContext` hook: + + ```diff + +import { usePickerContext } from '@mui/x-date-pickers/hooks'; + + // This contains a small behavior change. + // If the picker receives an invalid date, + // the old value would equal `null`. + // the new value would equal the invalid date received. + -const { value } = props; + +const { value } = usePickerContext(); + ``` + - The component passed to the `toolbar` slot no longer receives the `disabled` and `readOnly` props, instead you can use the `usePickerContext` hook: ```diff @@ -481,7 +520,17 @@ This change causes a few breaking changes: +const { readOnly } = usePickerContext(); ``` -- The component passed to the `toolbar` slot no longer receives a `view`, `views` and `onViewChange` props, instead you can use the `usePickerContext` hook: +- The component passed to the `layout` slot no longer receives the `isLandscape` prop, instead you can use the `usePickerContext` hook: + + ```diff + +import { usePickerContext } from '@mui/x-date-pickers/hooks'; + + -const { isLandscape } = props; + +const { orientation } = usePickerContext(); + +const isLandscape = orientation === 'landscape'; + ``` + +- The component passed to the `toolbar` slot no longer receives the `view`, `views` and `onViewChange` props, instead you can use the `usePickerContext` hook: ```diff +import { usePickerContext } from '@mui/x-date-pickers/hooks'; @@ -496,9 +545,28 @@ This change causes a few breaking changes: +const { onViewChange } = usePickerContext(); ``` +- The component passed to the `toolbar` slot no longer receives the `onChange` prop. + You can use the `usePickerActionsContext` or the `usePickerContext` hooks instead: + + ```diff + +import { usePickerActionsContext } from '@mui/x-date-pickers/hooks'; + + -const { onChange } = props; + -onChange(dayjs(), 'partial'); + -onChange(dayjs(), 'finish'); + +const { setValue } = usePickerActionsContext(); + +setValue(dayjs(), { changeImportance: 'set' }); + +setValue(dayjs(), { changeImportance: 'accept' }); + ``` + + :::success + The `usePickerContext` also contain all the actions returned by `usePickerActionsContext`. + The only difference is that `usePickerActionsContext` only contains variables with stable references that won't cause a re-render of your component. + ::: + ### Slot: `tabs` -- The component passed to the `tabs` slot no longer receives a `view`, `views` and `onViewChange` props, instead you can use the `usePickerContext` hook: +- The component passed to the `tabs` slot no longer receives the `view`, `views` and `onViewChange` props, instead you can use the `usePickerContext` hook: ```diff +import { usePickerContext } from '@mui/x-date-pickers/hooks'; @@ -515,7 +583,8 @@ This change causes a few breaking changes: ### Slot: `actionBar` -- The component passed to the `actionBar` slot no longer receives the `onClear`, `onSetToday`, `onAccept` and `onCancel` props. You can use the `usePickerActionsContext` or the `usePickerContext` hooks instead: +- The component passed to the `actionBar` slot no longer receives the `onClear`, `onSetToday`, `onAccept` and `onCancel` props. + You can use the `usePickerActionsContext` or the `usePickerContext` hooks instead: ```diff +import { usePickerActionsContext } from '@mui/x-date-pickers/hooks'; @@ -538,6 +607,44 @@ This change causes a few breaking changes: The only difference is that `usePickerActionsContext` only contains variables with stable references that won't cause a re-render of your component. ::: +### Slot: `shortcuts` + +- The component passed to the `shortcuts` slot no longer receives the `isLandscape` prop, instead you can use the `usePickerContext` hook: + + ```diff + +import { usePickerContext } from '@mui/x-date-pickers/hooks'; + + -const { isLandscape } = props; + +const { orientation } = usePickerContext(); + +const isLandscape = orientation === 'landscape'; + ``` + +- The component passed to the `shortcuts` slot no longer receives the `onChange` prop. + You can use the `usePickerActionsContext` or the `usePickerContext` hooks instead: + + ```diff + -const { onChange } = props; + -onChange(dayjs(), 'accept', myShortcut); + +const { setValue } = usePickerActionsContext(); + +setValue(dayjs(), { changeImportance: 'accept', shortcut: myShortcut }); + ``` + + :::success + The `usePickerContext` also contain all the actions returned by `usePickerActionsContext`. + The only difference is that `usePickerActionsContext` only contains variables with stable references that won't cause a re-render of your component. + ::: + +- The component passed to the `shortcuts` slot no longer receives the `isValid` prop, instead you can use the `useIsValidValue` hook: + + ```diff + +import { useIsValidValue } from '@mui/x-date-pickers/hooks'; + + -const { isValid } = props; + -const isTodayValid = isValid(dayjs()); + +const isValidValue = useIsValidValue(); + +const isTodayValid = isValidValue(dayjs()); + ``` + ## Renamed variables and types The following variables and types have been renamed to have a coherent `Picker` / `Pickers` prefix: @@ -584,7 +691,7 @@ The following variables and types have been renamed to have a coherent `Picker` +import { PickerValueType } from '@mui/x-date-pickers-pro'; ``` - - `RangeFieldSection` +- `RangeFieldSection` ```diff -import { RangeFieldSection } from '@mui/x-date-pickers-pro/models'; @@ -594,6 +701,16 @@ The following variables and types have been renamed to have a coherent `Picker` +import { FieldRangeSection } from '@mui/x-date-pickers-pro'; ``` +- `PickerShortcutChangeImportance` + + ```diff + -import { PickerShortcutChangeImportance } from '@mui/x-date-pickers/PickersShortcuts'; + -import { PickerShortcutChangeImportance } from '@mui/x-date-pickers'; + + +import { PickerChangeImportance } from '@mui/x-date-pickers/models'; + +import { PickerChangeImportance } from '@mui/x-date-pickers'; + ``` + ## Hooks breaking changes ### `usePickerContext` diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerToolbar.tsx b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerToolbar.tsx index 2019d54515ac..8e69b473b94a 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerToolbar.tsx +++ b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerToolbar.tsx @@ -15,7 +15,8 @@ import { PickerToolbarOwnerState, useToolbarOwnerState, } from '@mui/x-date-pickers/internals'; -import { usePickerTranslations } from '@mui/x-date-pickers/hooks'; +import { usePickerContext, usePickerTranslations } from '@mui/x-date-pickers/hooks'; +import { PickerValidDate } from '@mui/x-date-pickers/models'; import { UseRangePositionResponse } from '../internals/hooks/useRangePosition'; import { DateRangePickerToolbarClasses, @@ -33,7 +34,7 @@ const useUtilityClasses = (classes: Partial | und export interface DateRangePickerToolbarProps extends ExportedDateRangePickerToolbarProps, - Omit, 'onChange' | 'isLandscape'>, + Omit, Pick {} export interface ExportedDateRangePickerToolbarProps extends ExportedBaseToolbarProps { @@ -81,47 +82,49 @@ const DateRangePickerToolbar = React.forwardRef(function DateRangePickerToolbar( const props = useThemeProps({ props: inProps, name: 'MuiDateRangePickerToolbar' }); const { - value: [start, end], rangePosition, onRangePositionChange, - toolbarFormat, + toolbarFormat: toolbarFormatProp, className, classes: classesProp, ...other } = props; + const { value } = usePickerContext(); const translations = usePickerTranslations(); const ownerState = useToolbarOwnerState(); const classes = useUtilityClasses(classesProp); - const startDateValue = start - ? utils.formatByString(start, toolbarFormat || utils.formats.shortDate) - : translations.start; + // This can't be a default value when spreading because it breaks the API generation. + const toolbarFormat = toolbarFormatProp ?? utils.formats.shortDate; - const endDateValue = end - ? utils.formatByString(end, toolbarFormat || utils.formats.shortDate) - : translations.end; + const formatDate = (date: PickerValidDate | null, fallback: string) => { + if (date == null || !utils.isValid(date)) { + return fallback; + } + + return utils.formatByString(date, toolbarFormat); + }; return ( onRangePositionChange('start')} />  {'–'}  onRangePositionChange('end')} /> @@ -165,7 +168,6 @@ DateRangePickerToolbar.propTypes = { * @default "––" */ toolbarPlaceholder: PropTypes.node, - value: PropTypes.arrayOf(PropTypes.object).isRequired, } as any; export { DateRangePickerToolbar }; diff --git a/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx b/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx index 64a761e663a0..f0b086455fa5 100644 --- a/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx +++ b/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx @@ -37,7 +37,7 @@ const useUtilityClasses = (classes: Partial | type DateTimeRangeViews = Exclude; export interface DateTimeRangePickerToolbarProps - extends BaseToolbarProps, + extends BaseToolbarProps, Pick, ExportedDateTimeRangePickerToolbarProps { ampm?: boolean; @@ -88,14 +88,11 @@ const DateTimeRangePickerToolbar = React.forwardRef(function DateTimeRangePicker const utils = useUtils(); const { - value: [start, end], rangePosition, onRangePositionChange, className, classes: classesProp, - onChange, classes: inClasses, - isLandscape, ampm, hidden, toolbarFormat, @@ -105,13 +102,15 @@ const DateTimeRangePickerToolbar = React.forwardRef(function DateTimeRangePicker ...other } = props; - const { disabled, readOnly, view, onViewChange, views } = usePickerContext(); + const { value, setValue, disabled, readOnly, view, onViewChange, views } = usePickerContext< + PickerRangeValue, + DateTimeRangeViews + >(); const translations = usePickerTranslations(); const ownerState = useToolbarOwnerState(); const classes = useUtilityClasses(classesProp); const commonToolbarProps = { - isLandscape, views, ampm, disabled, @@ -121,19 +120,19 @@ const DateTimeRangePickerToolbar = React.forwardRef(function DateTimeRangePicker toolbarPlaceholder, }; - const handleOnChange = React.useCallback( + const wrappedSetValue = React.useCallback( (newDate: PickerValidDate | null) => { const { nextSelection, newRange } = calculateRangeChange({ newDate, utils, - range: props.value, + range: value, rangePosition, allowRangeFlip: true, }); onRangePositionChange(nextSelection); - onChange(newRange); + setValue(newRange, { changeImportance: 'set' }); }, - [onChange, onRangePositionChange, props.value, rangePosition, utils], + [setValue, onRangePositionChange, value, rangePosition, utils], ); const startOverrides = React.useMemo(() => { @@ -148,11 +147,13 @@ const DateTimeRangePickerToolbar = React.forwardRef(function DateTimeRangePicker }; return { + value: value[0], + setValue: wrappedSetValue, forceDesktopVariant: true, onViewChange: handleStartRangeViewChange, view: rangePosition === 'start' ? view : null, }; - }, [rangePosition, view, onRangePositionChange, onViewChange]); + }, [value, wrappedSetValue, rangePosition, view, onRangePositionChange, onViewChange]); const endOverrides = React.useMemo(() => { const handleEndRangeViewChange = (newView: DateOrTimeViewWithMeridiem) => { @@ -166,11 +167,13 @@ const DateTimeRangePickerToolbar = React.forwardRef(function DateTimeRangePicker }; return { + value: value[1], + setValue: wrappedSetValue, forceDesktopVariant: true, onViewChange: handleEndRangeViewChange, view: rangePosition === 'end' ? view : null, }; - }, [rangePosition, view, onRangePositionChange, onViewChange]); + }, [value, wrappedSetValue, rangePosition, view, onRangePositionChange, onViewChange]); if (hidden) { return null; @@ -186,22 +189,18 @@ const DateTimeRangePickerToolbar = React.forwardRef(function DateTimeRangePicker > @@ -226,8 +225,6 @@ DateTimeRangePickerToolbar.propTypes = { * @default `true` for Desktop, `false` for Mobile. */ hidden: PropTypes.bool, - isLandscape: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, onRangePositionChange: PropTypes.func.isRequired, rangePosition: PropTypes.oneOf(['end', 'start']).isRequired, /** @@ -248,7 +245,6 @@ DateTimeRangePickerToolbar.propTypes = { * @default "––" */ toolbarPlaceholder: PropTypes.node, - value: PropTypes.arrayOf(PropTypes.object).isRequired, } as any; export { DateTimeRangePickerToolbar }; diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx b/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx index f3133425b48c..77f2fa03865e 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx @@ -90,7 +90,6 @@ export const useDesktopRangePicker = < } const { - layoutProps, providerProps, renderCurrentView, shouldRestoreFocus, @@ -223,12 +222,7 @@ export const useDesktopRangePicker = < shouldRestoreFocus={shouldRestoreFocus} reduceAnimations={reduceAnimations} > - + {renderCurrentView()} diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useEnrichedRangePickerFieldProps.ts b/packages/x-date-pickers-pro/src/internals/hooks/useEnrichedRangePickerFieldProps.ts index 99bd3516edfb..72e8f2ca501d 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useEnrichedRangePickerFieldProps.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useEnrichedRangePickerFieldProps.ts @@ -91,7 +91,7 @@ export interface UseEnrichedRangePickerFieldPropsParams< TView extends DateOrTimeViewWithMeridiem, TEnableAccessibleFieldDOMStructure extends boolean, TError, -> extends Pick, +> extends Pick, 'open' | 'setOpen'>, UseRangePositionResponse { variant: PickerVariant; fieldType: FieldType; diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx b/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx index bd1c7725cd01..9fb312014560 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx @@ -86,7 +86,6 @@ export const useMobileRangePicker = < } const { - layoutProps, providerProps, renderCurrentView, fieldProps: pickerFieldProps, @@ -213,12 +212,7 @@ export const useMobileRangePicker = < - + {renderCurrentView()} diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.tsx b/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.tsx index 3d4d0d6f0032..c45e88550915 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.tsx @@ -38,7 +38,7 @@ export const useStaticRangePicker = < const { rangePosition, onRangePositionChange } = useRangePosition(props); - const { layoutProps, providerProps, renderCurrentView } = usePicker< + const { providerProps, renderCurrentView } = usePicker< PickerRangeValue, TView, TExternalProps, @@ -69,7 +69,6 @@ export const useStaticRangePicker = < const renderPicker = () => ( , - ExportedDatePickerToolbarProps {} +export interface DatePickerToolbarProps extends BaseToolbarProps, ExportedDatePickerToolbarProps {} export interface ExportedDatePickerToolbarProps extends ExportedBaseToolbarProps { /** @@ -83,9 +81,6 @@ export const DatePickerToolbar = React.forwardRef(function DatePickerToolbar( ) { const props = useThemeProps({ props: inProps, name: 'MuiDatePickerToolbar' }); const { - value, - isLandscape, - onChange, toolbarFormat, toolbarPlaceholder = '––', className, @@ -93,13 +88,13 @@ export const DatePickerToolbar = React.forwardRef(function DatePickerToolbar( ...other } = props; const utils = useUtils(); - const { views } = usePickerContext(); + const { value, views, orientation } = usePickerContext(); const translations = usePickerTranslations(); const ownerState = useToolbarOwnerState(); const classes = useUtilityClasses(classesProp); const dateText = React.useMemo(() => { - if (!value) { + if (value == null || !utils.isValid(value)) { return toolbarPlaceholder; } @@ -112,14 +107,13 @@ export const DatePickerToolbar = React.forwardRef(function DatePickerToolbar( @@ -144,8 +138,6 @@ DatePickerToolbar.propTypes = { * @default `true` for Desktop, `false` for Mobile. */ hidden: PropTypes.bool, - isLandscape: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, /** * The system prop that allows defining system overrides as well as additional CSS styles. */ @@ -164,5 +156,4 @@ DatePickerToolbar.propTypes = { * @default "––" */ toolbarPlaceholder: PropTypes.node, - value: PropTypes.object, } as any; diff --git a/packages/x-date-pickers/src/DateTimePicker/DateTimePickerToolbar.tsx b/packages/x-date-pickers/src/DateTimePicker/DateTimePickerToolbar.tsx index 0c276412cdb3..f2ac5da4ba09 100644 --- a/packages/x-date-pickers/src/DateTimePicker/DateTimePickerToolbar.tsx +++ b/packages/x-date-pickers/src/DateTimePicker/DateTimePickerToolbar.tsx @@ -22,12 +22,13 @@ import { MULTI_SECTION_CLOCK_SECTION_WIDTH } from '../internals/constants/dimens import { formatMeridiem } from '../internals/utils/date-utils'; import { pickersToolbarTextClasses } from '../internals/components/pickersToolbarTextClasses'; import { pickersToolbarClasses } from '../internals/components/pickersToolbarClasses'; -import { PickerValidDate } from '../models'; +import { AdapterFormats, DateTimeValidationError } from '../models'; import { usePickerContext } from '../hooks/usePickerContext'; import { PickerToolbarOwnerState, useToolbarOwnerState, } from '../internals/hooks/useToolbarOwnerState'; +import { SetValueActionOptions } from '../internals/hooks/usePicker/usePickerValue.types'; export interface ExportedDateTimePickerToolbarProps extends ExportedBaseToolbarProps { /** @@ -38,7 +39,7 @@ export interface ExportedDateTimePickerToolbarProps extends ExportedBaseToolbarP export interface DateTimePickerToolbarProps extends ExportedDateTimePickerToolbarProps, - BaseToolbarProps { + BaseToolbarProps { /** * If provided, it will be used instead of `dateTimePickerToolbarTitle` from localization. */ @@ -239,6 +240,8 @@ const DateTimePickerToolbarAmPmSelection = styled('div', { * This is used by the Date Time Range Picker Toolbar. */ export const DateTimePickerToolbarOverrideContext = React.createContext<{ + value: PickerValue; + setValue: (value: PickerValue, options?: SetValueActionOptions) => void; forceDesktopVariant: boolean; onViewChange: (view: DateOrTimeViewWithMeridiem) => void; view: DateOrTimeViewWithMeridiem | null; @@ -259,9 +262,6 @@ function DateTimePickerToolbar(inProps: DateTimePickerToolbarProps) { const { ampm, ampmInClock, - value, - onChange, - isLandscape, toolbarFormat, toolbarPlaceholder = '––', toolbarTitle: inToolbarTitle, @@ -271,33 +271,39 @@ function DateTimePickerToolbar(inProps: DateTimePickerToolbarProps) { } = props; const { + value: valueContext, + setValue: setValueContext, disabled, readOnly, variant, - view: viewCtx, - onViewChange: onViewChangeCtx, + orientation, + view: viewContext, + onViewChange: onViewChangeContext, views, } = usePickerContext(); + + const translations = usePickerTranslations(); const ownerState = useToolbarOwnerState(); const classes = useUtilityClasses(classesProp, ownerState); const utils = useUtils(); - const { meridiemMode, handleMeridiemChange } = useMeridiemMode(value, ampm, onChange); - const translations = usePickerTranslations(); const overrides = React.useContext(DateTimePickerToolbarOverrideContext); + const value = overrides ? overrides.value : valueContext; + const setValue = overrides ? overrides.setValue : setValueContext; + const view = overrides ? overrides.view : viewContext; + const onViewChange = overrides ? overrides.onViewChange : onViewChangeContext; + + const { meridiemMode, handleMeridiemChange } = useMeridiemMode(value, ampm, (newValue) => + setValue(newValue, { changeImportance: 'set' }), + ); + const toolbarVariant = overrides?.forceDesktopVariant ? 'desktop' : variant; const isDesktop = toolbarVariant === 'desktop'; const showAmPmControl = Boolean(ampm && !ampmInClock); const toolbarTitle = inToolbarTitle ?? translations.dateTimePickerToolbarTitle; - const view = overrides ? overrides.view : viewCtx; - const onViewChange = overrides ? overrides.onViewChange : onViewChangeCtx; - - const formatHours = (time: PickerValidDate) => - ampm ? utils.format(time, 'hours12h') : utils.format(time, 'hours24h'); - const dateText = React.useMemo(() => { - if (!value) { + if (value == null || !utils.isValid(value)) { return toolbarPlaceholder; } @@ -308,9 +314,16 @@ function DateTimePickerToolbar(inProps: DateTimePickerToolbarProps) { return utils.format(value, 'shortDate'); }, [value, toolbarFormat, toolbarPlaceholder, utils]); + const formatSection = (format: keyof AdapterFormats, fallback: string) => { + if (value == null || !utils.isValid(value)) { + return fallback; + } + + return utils.format(value, format); + }; + return ( onViewChange('year')} selected={view === 'year'} - value={value ? utils.format(value, 'year') : '–'} + value={formatSection('year', '–')} /> )} @@ -354,11 +367,15 @@ function DateTimePickerToolbar(inProps: DateTimePickerToolbarProps) { onViewChange('hours')} selected={view === 'hours'} - value={value ? formatHours(value) : '--'} + value={formatSection(ampm ? 'hours12h' : 'hours24h', '--')} /> onViewChange('minutes')} selected={view === 'minutes' || (!views.includes('minutes') && view === 'hours')} - value={value ? utils.format(value, 'minutes') : '--'} + value={formatSection('minutes', '--')} disabled={!views.includes('minutes')} /> @@ -390,11 +411,15 @@ function DateTimePickerToolbar(inProps: DateTimePickerToolbarProps) { /> onViewChange('seconds')} selected={view === 'seconds'} - value={value ? utils.format(value, 'seconds') : '--'} + value={formatSection('seconds', '--')} /> )} @@ -455,8 +480,6 @@ DateTimePickerToolbar.propTypes = { * @default `true` for Desktop, `false` for Mobile. */ hidden: PropTypes.bool, - isLandscape: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, /** * The system prop that allows defining system overrides as well as additional CSS styles. */ @@ -479,7 +502,6 @@ DateTimePickerToolbar.propTypes = { * If provided, it will be used instead of `dateTimePickerToolbarTitle` from localization. */ toolbarTitle: PropTypes.node, - value: PropTypes.object, } as any; export { DateTimePickerToolbar }; diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePickerLayout.tsx b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePickerLayout.tsx index cafef574827f..dbc84f15bf76 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePickerLayout.tsx +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePickerLayout.tsx @@ -67,9 +67,6 @@ DesktopDateTimePickerLayout.propTypes = { */ classes: PropTypes.object, className: PropTypes.string, - isValid: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onSelectShortcut: PropTypes.func.isRequired, /** * The props used for each component slot. * @default {} @@ -88,7 +85,6 @@ DesktopDateTimePickerLayout.propTypes = { PropTypes.func, PropTypes.object, ]), - value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), } as any; export { DesktopDateTimePickerLayout }; diff --git a/packages/x-date-pickers/src/PickersActionBar/PickersActionBar.test.tsx b/packages/x-date-pickers/src/PickersActionBar/PickersActionBar.test.tsx index b9f6e46a11ca..4c95cce547de 100644 --- a/packages/x-date-pickers/src/PickersActionBar/PickersActionBar.test.tsx +++ b/packages/x-date-pickers/src/PickersActionBar/PickersActionBar.test.tsx @@ -11,6 +11,7 @@ describe('', () => { const renderWithContext = (element: React.ReactElement) => { const spys = { + setValue: spy(), setOpen: spy(), clearValue: spy(), setValueToToday: spy(), diff --git a/packages/x-date-pickers/src/PickersLayout/PickersLayout.tsx b/packages/x-date-pickers/src/PickersLayout/PickersLayout.tsx index bccc518ae565..6f1a8e423330 100644 --- a/packages/x-date-pickers/src/PickersLayout/PickersLayout.tsx +++ b/packages/x-date-pickers/src/PickersLayout/PickersLayout.tsx @@ -150,9 +150,6 @@ PickersLayout.propTypes = { */ classes: PropTypes.object, className: PropTypes.string, - isValid: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onSelectShortcut: PropTypes.func.isRequired, /** * The props used for each component slot. * @default {} @@ -171,7 +168,6 @@ PickersLayout.propTypes = { PropTypes.func, PropTypes.object, ]), - value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), } as any; export { PickersLayout }; diff --git a/packages/x-date-pickers/src/PickersLayout/PickersLayout.types.ts b/packages/x-date-pickers/src/PickersLayout/PickersLayout.types.ts index c7e2e388edce..da7fce5942eb 100644 --- a/packages/x-date-pickers/src/PickersLayout/PickersLayout.types.ts +++ b/packages/x-date-pickers/src/PickersLayout/PickersLayout.types.ts @@ -12,7 +12,6 @@ import { } from '../PickersShortcuts/PickersShortcuts'; import { PickerOwnerState } from '../models'; import { PickerValidValue } from '../internals/models'; -import { UsePickerValueLayoutResponse } from '../internals/hooks/usePicker/usePickerValue.types'; export interface ExportedPickersLayoutSlots { /** @@ -69,7 +68,7 @@ export interface PickersLayoutSlots * Custom component for the toolbar. * It is placed above the picker views. */ - toolbar?: React.JSXElementConstructor>; + toolbar?: React.JSXElementConstructor; } export interface PickersLayoutSlotProps @@ -84,8 +83,7 @@ export interface PickersLayoutSlotProps toolbar?: ExportedBaseToolbarProps; } -export interface PickersLayoutProps - extends UsePickerValueLayoutResponse { +export interface PickersLayoutProps { className?: string; children?: React.ReactNode; /** diff --git a/packages/x-date-pickers/src/PickersLayout/usePickerLayout.tsx b/packages/x-date-pickers/src/PickersLayout/usePickerLayout.tsx index 33cc7113d0dc..5c7fd96a32c5 100644 --- a/packages/x-date-pickers/src/PickersLayout/usePickerLayout.tsx +++ b/packages/x-date-pickers/src/PickersLayout/usePickerLayout.tsx @@ -12,9 +12,7 @@ import { PickerValidValue } from '../internals/models'; import { usePickerPrivateContext } from '../internals/hooks/usePickerPrivateContext'; import { usePickerContext } from '../hooks'; -function toolbarHasView( - toolbarProps: BaseToolbarProps | any, -): toolbarProps is BaseToolbarProps { +function toolbarHasView(toolbarProps: BaseToolbarProps | any): toolbarProps is BaseToolbarProps { return toolbarProps.view !== null; } @@ -44,23 +42,10 @@ const usePickerLayout = ( props: PickersLayoutProps, ): UsePickerLayoutResponse => { const { ownerState: pickerOwnerState } = usePickerPrivateContext(); - const { variant, orientation, view } = usePickerContext(); + const { variant, view } = usePickerContext(); const isRtl = useRtl(); - const { - value, - onChange, - onSelectShortcut, - isValid, - children, - slots, - slotProps, - classes: classesProp, - // TODO: Remove this "as" hack. It get introduced to mark `value` prop in PickersLayoutProps as not required. - // The true type should be - // - For pickers value: PickerValidDate | null - // - For range pickers value: [PickerValidDate | null, PickerValidDate | null] - } = props; + const { children, slots, slotProps, classes: classesProp } = props; const ownerState = React.useMemo( () => ({ ...pickerOwnerState, layoutDirection: isRtl ? 'rtl' : 'ltr' }), @@ -86,11 +71,6 @@ const usePickerLayout = ( const toolbarProps = useSlotProps({ elementType: Toolbar!, externalSlotProps: slotProps?.toolbar, - additionalProps: { - isLandscape: orientation === 'landscape', // Will be removed in a follow up PR? - onChange, - value, - }, className: classes.toolbar, ownerState, }); @@ -108,11 +88,6 @@ const usePickerLayout = ( const shortcutsProps = useSlotProps({ elementType: Shortcuts!, externalSlotProps: slotProps?.shortcuts, - additionalProps: { - isValid, - isLandscape: orientation === 'landscape', // Will be removed in a follow up PR? - onChange: onSelectShortcut, - }, className: classes.shortcuts, ownerState, }); diff --git a/packages/x-date-pickers/src/PickersShortcuts/PickersShortcuts.tsx b/packages/x-date-pickers/src/PickersShortcuts/PickersShortcuts.tsx index 3f31edfc3ca6..9fbd3921ab74 100644 --- a/packages/x-date-pickers/src/PickersShortcuts/PickersShortcuts.tsx +++ b/packages/x-date-pickers/src/PickersShortcuts/PickersShortcuts.tsx @@ -7,6 +7,8 @@ import ListItem from '@mui/material/ListItem'; import Chip from '@mui/material/Chip'; import { VIEW_HEIGHT } from '../internals/constants/dimensions'; import { PickerValidValue } from '../internals/models'; +import { useIsValidValue, usePickerActionsContext } from '../hooks'; +import { PickerChangeImportance } from '../models/pickers'; interface PickersShortcutsItemGetValueParams { isValid: (value: TValue) => boolean; @@ -24,8 +26,6 @@ export interface PickersShortcutsItem { export type PickersShortcutsItemContext = Omit, 'getValue'>; -export type PickerShortcutChangeImportance = 'set' | 'accept'; - export interface ExportedPickersShortcutProps extends Omit { /** @@ -40,19 +40,11 @@ export interface ExportedPickersShortcutProps * - "set": fires `onChange` but do not fire `onAccept` and does not close the picker. * @default "accept" */ - changeImportance?: PickerShortcutChangeImportance; + changeImportance?: PickerChangeImportance; } export interface PickersShortcutsProps - extends ExportedPickersShortcutProps { - isLandscape: boolean; - onChange: ( - newValue: TValue, - changeImportance: PickerShortcutChangeImportance, - shortcut: PickersShortcutsItemContext, - ) => void; - isValid: (value: TValue) => boolean; -} + extends ExportedPickersShortcutProps {} const PickersShortcutsRoot = styled(List, { name: 'MuiPickersLayout', @@ -70,22 +62,25 @@ const PickersShortcutsRoot = styled(List, { * - [PickersShortcuts API](https://mui.com/x/api/date-pickers/pickers-shortcuts/) */ function PickersShortcuts(props: PickersShortcutsProps) { - const { items, changeImportance = 'accept', isLandscape, onChange, isValid, ...other } = props; + const { items, changeImportance = 'accept', ...other } = props; + + const { setValue } = usePickerActionsContext(); + const isValidValue = useIsValidValue(); if (items == null || items.length === 0) { return null; } const resolvedItems = items.map(({ getValue, ...item }) => { - const newValue = getValue({ isValid }); + const newValue = getValue({ isValid: isValidValue }); return { ...item, label: item.label, onClick: () => { - onChange(newValue, changeImportance, item); + setValue(newValue, { changeImportance, shortcut: item }); }, - disabled: !isValid(newValue), + disabled: !isValidValue(newValue), }; }); @@ -139,8 +134,6 @@ PickersShortcuts.propTypes = { * @default false */ disablePadding: PropTypes.bool, - isLandscape: PropTypes.bool.isRequired, - isValid: PropTypes.func.isRequired, /** * Ordered array of shortcuts to display. * If empty, does not display the shortcuts. @@ -153,7 +146,6 @@ PickersShortcuts.propTypes = { label: PropTypes.string.isRequired, }), ), - onChange: PropTypes.func.isRequired, style: PropTypes.object, /** * The content of the subheader, normally `ListSubheader`. diff --git a/packages/x-date-pickers/src/PickersShortcuts/index.ts b/packages/x-date-pickers/src/PickersShortcuts/index.ts index 594681ba291b..551eb4ef3fdf 100644 --- a/packages/x-date-pickers/src/PickersShortcuts/index.ts +++ b/packages/x-date-pickers/src/PickersShortcuts/index.ts @@ -3,5 +3,4 @@ export type { PickersShortcutsProps, PickersShortcutsItem, PickersShortcutsItemContext, - PickerShortcutChangeImportance, } from './PickersShortcuts'; diff --git a/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx b/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx index 3dce9ed96856..e1acd81dc635 100644 --- a/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx +++ b/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx @@ -19,16 +19,14 @@ import { } from './timePickerToolbarClasses'; import { PickerValue, TimeViewWithMeridiem } from '../internals/models'; import { formatMeridiem } from '../internals/utils/date-utils'; -import { PickerValidDate } from '../models'; +import { AdapterFormats } from '../models'; import { usePickerContext } from '../hooks'; import { PickerToolbarOwnerState, useToolbarOwnerState, } from '../internals/hooks/useToolbarOwnerState'; -export interface TimePickerToolbarProps - extends BaseToolbarProps, - ExportedTimePickerToolbarProps { +export interface TimePickerToolbarProps extends BaseToolbarProps, ExportedTimePickerToolbarProps { ampm?: boolean; ampmInClock?: boolean; } @@ -154,28 +152,28 @@ const TimePickerToolbarAmPmSelection = styled('div', { */ function TimePickerToolbar(inProps: TimePickerToolbarProps) { const props = useThemeProps({ props: inProps, name: 'MuiTimePickerToolbar' }); - const { - ampm, - ampmInClock, - value, - isLandscape, - onChange, - className, - classes: classesProp, - ...other - } = props; + const { ampm, ampmInClock, className, classes: classesProp, ...other } = props; const utils = useUtils(); const translations = usePickerTranslations(); const ownerState = useToolbarOwnerState(); const classes = useUtilityClasses(classesProp, ownerState); - const { disabled, readOnly, view, onViewChange, views } = - usePickerContext(); + const { value, setValue, disabled, readOnly, view, onViewChange, views } = usePickerContext< + PickerValue, + TimeViewWithMeridiem + >(); const showAmPmControl = Boolean(ampm && !ampmInClock && views.includes('hours')); - const { meridiemMode, handleMeridiemChange } = useMeridiemMode(value, ampm, onChange); + const { meridiemMode, handleMeridiemChange } = useMeridiemMode(value, ampm, (newValue) => + setValue(newValue, { changeImportance: 'set' }), + ); + + const formatSection = (format: keyof AdapterFormats) => { + if (value == null || !utils.isValid(value)) { + return '--'; + } - const formatHours = (time: PickerValidDate) => - ampm ? utils.format(time, 'hours12h') : utils.format(time, 'hours24h'); + return utils.formatByString(value, format); + }; const separator = ( onViewChange('hours')} selected={view === 'hours'} - value={value ? formatHours(value) : '--'} + value={formatSection(ampm ? 'hours12h' : 'hours24h')} /> )} @@ -216,7 +213,7 @@ function TimePickerToolbar(inProps: TimePickerToolbarProps) { variant="h3" onClick={() => onViewChange('minutes')} selected={view === 'minutes'} - value={value ? utils.format(value, 'minutes') : '--'} + value={formatSection('minutes')} /> )} @@ -227,7 +224,7 @@ function TimePickerToolbar(inProps: TimePickerToolbarProps) { variant="h3" onClick={() => onViewChange('seconds')} selected={view === 'seconds'} - value={value ? utils.format(value, 'seconds') : '--'} + value={formatSection('seconds')} /> )} @@ -276,8 +273,6 @@ TimePickerToolbar.propTypes = { * @default `true` for Desktop, `false` for Mobile. */ hidden: PropTypes.bool, - isLandscape: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, /** * The system prop that allows defining system overrides as well as additional CSS styles. */ @@ -296,7 +291,6 @@ TimePickerToolbar.propTypes = { * @default "––" */ toolbarPlaceholder: PropTypes.node, - value: PropTypes.object, } as any; export { TimePickerToolbar }; diff --git a/packages/x-date-pickers/src/hooks/index.tsx b/packages/x-date-pickers/src/hooks/index.tsx index d1aadefbed6b..a367782490ce 100644 --- a/packages/x-date-pickers/src/hooks/index.tsx +++ b/packages/x-date-pickers/src/hooks/index.tsx @@ -11,3 +11,4 @@ export { useSplitFieldProps } from './useSplitFieldProps'; export { useParsedFormat } from './useParsedFormat'; export { usePickerContext } from './usePickerContext'; export { usePickerActionsContext } from './usePickerActionsContext'; +export { useIsValidValue } from './useIsValidValue'; diff --git a/packages/x-date-pickers/src/hooks/useIsValidValue.tsx b/packages/x-date-pickers/src/hooks/useIsValidValue.tsx new file mode 100644 index 000000000000..04169b19a75f --- /dev/null +++ b/packages/x-date-pickers/src/hooks/useIsValidValue.tsx @@ -0,0 +1,12 @@ +'use client'; +import * as React from 'react'; +import { PickerValidValue } from '../internals/models'; + +export const IsValidValueContext = React.createContext<(value: any) => boolean>(() => true); + +/** + * Returns a function to check if a value is valid according to the validation props passed to the parent picker. + */ +export function useIsValidValue() { + return React.useContext(IsValidValueContext) as (value: TValue) => boolean; +} diff --git a/packages/x-date-pickers/src/hooks/usePickerActionsContext.ts b/packages/x-date-pickers/src/hooks/usePickerActionsContext.ts index 0b90539b2b9a..50d1155790b9 100644 --- a/packages/x-date-pickers/src/hooks/usePickerActionsContext.ts +++ b/packages/x-date-pickers/src/hooks/usePickerActionsContext.ts @@ -1,13 +1,23 @@ 'use client'; import * as React from 'react'; -import { PickerActionsContext } from '../internals/components/PickerProvider'; +import { + PickerActionsContext, + PickerActionsContextValue, +} from '../internals/components/PickerProvider'; +import { PickerValidValue, PickerValue } from '../internals/models'; /** * Returns a subset of the context passed by the picker wrapping the current component. * It only contains the actions and never causes a re-render of the component using it. */ -export const usePickerActionsContext = () => { - const value = React.useContext(PickerActionsContext); +export const usePickerActionsContext = < + TValue extends PickerValidValue = PickerValue, + TError = string, +>() => { + const value = React.useContext(PickerActionsContext) as PickerActionsContextValue< + TValue, + TError + > | null; if (value == null) { throw new Error( [ diff --git a/packages/x-date-pickers/src/hooks/usePickerContext.ts b/packages/x-date-pickers/src/hooks/usePickerContext.ts index 61d4ffe9f4b1..8383c11d398c 100644 --- a/packages/x-date-pickers/src/hooks/usePickerContext.ts +++ b/packages/x-date-pickers/src/hooks/usePickerContext.ts @@ -1,20 +1,20 @@ 'use client'; import * as React from 'react'; import { PickerContext, PickerContextValue } from '../internals/components/PickerProvider'; -import { DateOrTimeViewWithMeridiem } from '../internals/models'; +import { DateOrTimeViewWithMeridiem, PickerValidValue, PickerValue } from '../internals/models'; /** * Returns the context passed by the picker that wraps the current component. */ export const usePickerContext = < + TValue extends PickerValidValue = PickerValue, TView extends DateOrTimeViewWithMeridiem = DateOrTimeViewWithMeridiem, + TError = string, >() => { - const value = React.useContext(PickerContext) as PickerContextValue; + const value = React.useContext(PickerContext) as PickerContextValue; if (value == null) { throw new Error( - [ - 'MUI X: The `usePickerContext` can only be called in fields that are used as a slot of a picker component', - ].join('\n'), + 'MUI X: The `usePickerContext` hook can only be called inside the context of a picker component', ); } diff --git a/packages/x-date-pickers/src/internals/components/PickerProvider.tsx b/packages/x-date-pickers/src/internals/components/PickerProvider.tsx index 95238191b4a0..e3642b186fdb 100644 --- a/packages/x-date-pickers/src/internals/components/PickerProvider.tsx +++ b/packages/x-date-pickers/src/internals/components/PickerProvider.tsx @@ -2,17 +2,25 @@ import * as React from 'react'; import { PickerOwnerState } from '../../models'; import { PickersInputLocaleText } from '../../locales'; import { LocalizationProvider } from '../../LocalizationProvider'; -import { DateOrTimeViewWithMeridiem, PickerOrientation, PickerVariant } from '../models'; +import { + DateOrTimeViewWithMeridiem, + PickerOrientation, + PickerValidValue, + PickerVariant, +} from '../models'; import type { UsePickerValueActionsContextValue, UsePickerValueContextValue, UsePickerValuePrivateContextValue, } from '../hooks/usePicker/usePickerValue.types'; import { UsePickerViewsContextValue } from '../hooks/usePicker/usePickerViews'; +import { IsValidValueContext } from '../../hooks/useIsValidValue'; -export const PickerContext = React.createContext | null>(null); +export const PickerContext = React.createContext | null>(null); -export const PickerActionsContext = React.createContext(null); +export const PickerActionsContext = React.createContext | null>( + null, +); export const PickerPrivateContext = React.createContext({ ownerState: { @@ -33,31 +41,45 @@ export const PickerPrivateContext = React.createContext( + props: PickerProviderProps, +) { + const { + contextValue, + actionsContextValue, + privateContextValue, + isValidContextValue, + localeText, + children, + } = props; return ( - {children} + + {children} + ); } -export interface PickerProviderProps { - contextValue: PickerContextValue; - actionsContextValue: PickerActionsContextValue; +export interface PickerProviderProps { + contextValue: PickerContextValue; + actionsContextValue: PickerActionsContextValue; privateContextValue: PickerPrivateContextValue; + isValidContextValue: (value: TValue) => boolean; localeText: PickersInputLocaleText | undefined; children: React.ReactNode; } export interface PickerContextValue< - TView extends DateOrTimeViewWithMeridiem = DateOrTimeViewWithMeridiem, -> extends UsePickerValueContextValue, + TValue extends PickerValidValue, + TView extends DateOrTimeViewWithMeridiem, + TError, +> extends UsePickerValueContextValue, UsePickerViewsContextValue { /** * `true` if the picker is disabled, `false` otherwise. @@ -86,7 +108,8 @@ export interface PickerContextValue< orientation: PickerOrientation; } -export interface PickerActionsContextValue extends UsePickerValueActionsContextValue {} +export interface PickerActionsContextValue + extends UsePickerValueActionsContextValue {} export interface PickerPrivateContextValue extends UsePickerValuePrivateContextValue { /** diff --git a/packages/x-date-pickers/src/internals/components/PickersToolbar.tsx b/packages/x-date-pickers/src/internals/components/PickersToolbar.tsx index cee826d3cd7d..b956c7d4e3ec 100644 --- a/packages/x-date-pickers/src/internals/components/PickersToolbar.tsx +++ b/packages/x-date-pickers/src/internals/components/PickersToolbar.tsx @@ -6,11 +6,9 @@ import composeClasses from '@mui/utils/composeClasses'; import { shouldForwardProp } from '@mui/system/createStyled'; import { BaseToolbarProps } from '../models/props/toolbar'; import { getPickersToolbarUtilityClass, PickersToolbarClasses } from './pickersToolbarClasses'; -import { PickerValidValue } from '../models'; import { PickerToolbarOwnerState, useToolbarOwnerState } from '../hooks/useToolbarOwnerState'; -export interface PickersToolbarProps - extends Pick, 'isLandscape' | 'hidden' | 'titleId'> { +export interface PickersToolbarProps extends Pick { className?: string; landscapeDirection?: 'row' | 'column'; toolbarTitle: React.ReactNode; @@ -84,13 +82,14 @@ const PickersToolbarContent = styled('div', { ], }); -type PickersToolbarComponent = (( - props: React.PropsWithChildren> & React.RefAttributes, +type PickersToolbarComponent = (( + props: React.PropsWithChildren & React.RefAttributes, ) => React.JSX.Element) & { propTypes?: any }; -export const PickersToolbar = React.forwardRef(function PickersToolbar< - TValue extends PickerValidValue, ->(inProps: React.PropsWithChildren>, ref: React.Ref) { +export const PickersToolbar = React.forwardRef(function PickersToolbar( + inProps: React.PropsWithChildren, + ref: React.Ref, +) { const props = useThemeProps({ props: inProps, name: 'MuiPickersToolbar' }); const { children, @@ -99,7 +98,6 @@ export const PickersToolbar = React.forwardRef(function PickersToolbar< toolbarTitle, hidden, titleId, - isLandscape, classes: inClasses, landscapeDirection, ...other diff --git a/packages/x-date-pickers/src/internals/hooks/date-helpers-hooks.tsx b/packages/x-date-pickers/src/internals/hooks/date-helpers-hooks.tsx index 74b280b9e42c..3e7752b5e517 100644 --- a/packages/x-date-pickers/src/internals/hooks/date-helpers-hooks.tsx +++ b/packages/x-date-pickers/src/internals/hooks/date-helpers-hooks.tsx @@ -57,15 +57,20 @@ export function useMeridiemMode( selectionState?: PickerSelectionState, ) { const utils = useUtils(); - const meridiemMode = getMeridiem(date, utils); + const cleanDate = React.useMemo( + () => (date == null || !utils.isValid(date) ? null : date), + [utils, date], + ); + + const meridiemMode = getMeridiem(cleanDate, utils); const handleMeridiemChange = React.useCallback( (mode: 'am' | 'pm') => { const timeWithMeridiem = - date == null ? null : convertToMeridiem(date, mode, Boolean(ampm), utils); + cleanDate == null ? null : convertToMeridiem(cleanDate, mode, Boolean(ampm), utils); onChange(timeWithMeridiem, selectionState ?? 'partial'); }, - [ampm, date, onChange, selectionState, utils], + [ampm, cleanDate, onChange, selectionState, utils], ); return { meridiemMode, handleMeridiemChange }; diff --git a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx index f206939d69f8..66b8c6cb0b63 100644 --- a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx @@ -61,7 +61,6 @@ export const useDesktopPicker = < const { hasUIView, - layoutProps, providerProps, renderCurrentView, shouldRestoreFocus, @@ -209,7 +208,7 @@ export const useDesktopPicker = < shouldRestoreFocus={shouldRestoreFocus} reduceAnimations={reduceAnimations} > - + {renderCurrentView()} diff --git a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx index 101e2013d2cc..7b1babc50a59 100644 --- a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx @@ -56,7 +56,6 @@ export const useMobilePicker = < const isToolbarHidden = innerSlotProps?.toolbar?.hidden ?? false; const { - layoutProps, providerProps, renderCurrentView, fieldProps: pickerFieldProps, @@ -154,7 +153,7 @@ export const useMobilePicker = < unstableFieldRef={handleFieldRef} /> - + {renderCurrentView()} diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts index 46d94bdfa489..16daed938d42 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts @@ -71,11 +71,6 @@ export const usePicker = < hasUIView: pickerViewsResponse.provider.hasUIView, shouldRestoreFocus: pickerViewsResponse.shouldRestoreFocus, - // Picker layout - layoutProps: { - ...pickerValueResponse.layoutProps, - }, - // Picker provider providerProps, diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts index 34a81c4e0bf4..eb966802b722 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts @@ -10,7 +10,7 @@ import { UsePickerViewsResponse, UsePickerViewsBaseProps, } from './usePickerViews'; -import { PickerOwnerState } from '../../../models'; +import { InferError, PickerOwnerState } from '../../../models'; import { DateOrTimeViewWithMeridiem, PickerValidValue } from '../../models'; import { UsePickerProviderParameters, @@ -54,7 +54,7 @@ export interface UsePickerParams< UsePickerViewParams, 'additionalViewProps' | 'autoFocusView' | 'rendererInterceptor' | 'fieldRef' >, - Pick, 'localeText'> { + Pick>, 'localeText'> { props: TExternalProps; } @@ -65,8 +65,7 @@ export interface UsePickerResponse< > extends Pick, 'fieldProps'>, Pick, 'shouldRestoreFocus' | 'renderCurrentView'> { ownerState: PickerOwnerState; - providerProps: UsePickerProviderReturnValue; - layoutProps: UsePickerValueResponse['layoutProps']; + providerProps: UsePickerProviderReturnValue; // TODO v8: Remove in https://github.com/mui/mui-x/pull/15671 hasUIView: boolean; } diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerProvider.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerProvider.ts index 1d9260a09ee9..6cb0bfc5c21d 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerProvider.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerProvider.ts @@ -63,7 +63,10 @@ export const usePickerOrientation = ( export function usePickerProvider< TValue extends PickerValidValue, TView extends DateOrTimeViewWithMeridiem, ->(parameters: UsePickerProviderParameters): UsePickerProviderReturnValue { + TError, +>( + parameters: UsePickerProviderParameters, +): UsePickerProviderReturnValue { const { props, valueManager, @@ -101,7 +104,7 @@ export function usePickerProvider< ], ); - const contextValue = React.useMemo>( + const contextValue = React.useMemo>( () => ({ ...paramsFromUsePickerValue.contextValue, ...paramsFromUsePickerViews.contextValue, @@ -128,23 +131,26 @@ export function usePickerProvider< return { localeText, contextValue, - actionsContextValue: paramsFromUsePickerValue.actionsContextValue, privateContextValue, + actionsContextValue: paramsFromUsePickerValue.actionsContextValue, + isValidContextValue: paramsFromUsePickerValue.isValidContextValue, }; } export interface UsePickerProviderParameters< TValue extends PickerValidValue, TView extends DateOrTimeViewWithMeridiem, -> extends Pick { + TError, +> extends Pick, 'localeText'> { props: UsePickerProps; valueManager: PickerValueManager; variant: PickerVariant; - paramsFromUsePickerValue: UsePickerValueProviderParams; + paramsFromUsePickerValue: UsePickerValueProviderParams; paramsFromUsePickerViews: UsePickerViewsProviderParams; } -export interface UsePickerProviderReturnValue extends Omit {} +export interface UsePickerProviderReturnValue + extends Omit, 'children'> {} /** * Props used to create the picker's contexts. diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts index a3adeb812499..9309b7b6c21a 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts @@ -2,13 +2,8 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { useOpenState } from '../useOpenState'; import { useLocalizationContext, useUtils } from '../useUtils'; -import { FieldChangeHandlerContext } from '../useField'; import { useValidation } from '../../../validation'; import { PickerChangeHandlerContext, InferError } from '../../../models'; -import { - PickerShortcutChangeImportance, - PickersShortcutsItemContext, -} from '../../../PickersShortcuts'; import { UsePickerValueProps, UsePickerValueParams, @@ -16,7 +11,6 @@ import { PickerValueUpdateAction, UsePickerValueState, UsePickerValueFieldResponse, - UsePickerValueLayoutResponse, UsePickerValueViewsResponse, PickerSelectionState, PickerValueUpdaterParams, @@ -24,6 +18,7 @@ import { UsePickerValueProviderParams, UsePickerValueActionsContextValue, UsePickerValuePrivateContextValue, + SetValueActionOptions, } from './usePickerValue.types'; import { useValueWithTimezone } from '../useValueWithTimezone'; import { PickerValidValue } from '../../models'; @@ -39,11 +34,6 @@ const shouldPublishValue = ( 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. @@ -67,7 +57,7 @@ const shouldPublishValue = ( return hasChanged(dateState.lastPublishedValue); } - if (action.name === 'setValueFromShortcut') { + if (action.name === 'setExplicitValue') { // 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) { @@ -115,8 +105,8 @@ const shouldCommitValue = ( return hasChanged(dateState.lastCommittedValue); } - if (action.name === 'setValueFromShortcut') { - return action.changeImportance === 'accept' && hasChanged(dateState.lastCommittedValue); + if (action.name === 'setExplicitValue') { + return action.options.changeImportance === 'accept' && hasChanged(dateState.lastCommittedValue); } return false; @@ -138,8 +128,8 @@ const shouldClosePicker = ( return action.selectionState === 'finish' && closeOnSelect; } - if (action.name === 'setValueFromShortcut') { - return action.changeImportance === 'accept'; + if (action.name === 'setExplicitValue') { + return action.options.changeImportance === 'accept'; } return false; @@ -290,16 +280,18 @@ export const usePickerValue = < const getContext = (): PickerChangeHandlerContext => { if (!cachedContext) { const validationError = - action.name === 'setValueFromField' - ? action.context.validationError + action.name === 'setExplicitValue' && action.options.validationError != null + ? action.options.validationError : getValidationErrorForNewValue(action.value); cachedContext = { validationError, }; - if (action.name === 'setValueFromShortcut') { - cachedContext.shortcut = action.shortcut; + if (action.name === 'setExplicitValue') { + if (action.options.shortcut) { + cachedContext.shortcut = action.options.shortcut; + } } } @@ -345,30 +337,6 @@ export const usePickerValue = < updateDate({ name: 'setValueFromView', value: newValue, selectionState }), ); - const handleSelectShortcut = useEventCallback( - ( - newValue: TValue, - changeImportance: PickerShortcutChangeImportance, - shortcut: PickersShortcutsItemContext, - ) => - updateDate({ - name: 'setValueFromShortcut', - value: newValue, - changeImportance, - shortcut, - }), - ); - - const handleChangeFromField = useEventCallback( - (newValue: TValue, context: FieldChangeHandlerContext) => - updateDate({ name: 'setValueFromField', value: newValue, context }), - ); - - const fieldResponse: UsePickerValueFieldResponse = { - value: dateState.draft, - onChange: handleChangeFromField, - }; - const valueWithoutError = React.useMemo( () => valueManager.cleanValue(utils, dateState.draft), [utils, valueManager, dateState.draft], @@ -392,12 +360,13 @@ export const usePickerValue = < return !valueManager.hasError(error); }; - const layoutResponse: UsePickerValueLayoutResponse = { - value: valueWithoutError, - onChange: handleChange, - onSelectShortcut: handleSelectShortcut, - isValid, - }; + const setValue = useEventCallback((newValue: TValue, options?: SetValueActionOptions) => + updateDate({ + name: 'setExplicitValue', + value: newValue, + options: { changeImportance: 'accept', ...options }, + }), + ); const clearValue = useEventCallback(() => updateDate({ @@ -439,23 +408,31 @@ export const usePickerValue = < }); }); - const actionsContextValue = React.useMemo( + const fieldResponse: UsePickerValueFieldResponse = { + value: dateState.draft, + onChange: (newValue, context) => + setValue(newValue, { validationError: context.validationError }), + }; + + const actionsContextValue = React.useMemo>( () => ({ + setValue, setOpen, clearValue, setValueToToday, acceptValueChanges, cancelValueChanges, }), - [setOpen, clearValue, setValueToToday, acceptValueChanges, cancelValueChanges], + [setValue, setOpen, clearValue, setValueToToday, acceptValueChanges, cancelValueChanges], ); - const contextValue = React.useMemo( + const contextValue = React.useMemo>( () => ({ ...actionsContextValue, open, + value: dateState.draft, }), - [actionsContextValue, open], + [actionsContextValue, open, dateState.draft], ); const privateContextValue = React.useMemo( @@ -463,17 +440,17 @@ export const usePickerValue = < [dismissViews], ); - const providerParams: UsePickerValueProviderParams = { + const providerParams: UsePickerValueProviderParams = { value: valueWithoutError, contextValue, actionsContextValue, privateContextValue, + isValidContextValue: isValid, }; return { fieldProps: fieldResponse, viewProps: viewResponse, - layoutProps: layoutResponse, provider: providerParams, }; }; diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.types.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.types.ts index 9644f63baa90..03c78458091f 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.types.ts @@ -1,5 +1,6 @@ import * as React from 'react'; -import { FieldChangeHandlerContext, UseFieldInternalProps } from '../useField'; +import { MakeRequired } from '@mui/x-internals/types'; +import { UseFieldInternalProps } from '../useField'; import { Validator } from '../../../validation'; import { PickerVariant } from '../../models/common'; import { @@ -11,12 +12,10 @@ import { OnErrorProps, InferError, PickerValueType, + PickerChangeImportance, } from '../../../models'; import { GetDefaultReferenceDateProps } from '../../utils/getDefaultReferenceDate'; -import { - PickerShortcutChangeImportance, - PickersShortcutsItemContext, -} from '../../../PickersShortcuts'; +import type { PickersShortcutsItemContext } from '../../../PickersShortcuts'; import { InferNonNullablePickerValue, PickerValidValue } from '../../models'; export interface PickerValueManager { @@ -177,21 +176,15 @@ export type PickerValueUpdateAction = value: TValue; selectionState: PickerSelectionState; } - | { - name: 'setValueFromField'; - value: TValue; - context: FieldChangeHandlerContext; - } | { name: 'setValueFromAction'; value: TValue; pickerAction: 'accept' | 'today' | 'cancel' | 'dismiss' | 'clear'; } | { - name: 'setValueFromShortcut'; + name: 'setExplicitValue'; value: TValue; - changeImportance: PickerShortcutChangeImportance; - shortcut: PickersShortcutsItemContext; + options: MakeRequired, 'changeImportance'>; }; /** @@ -289,45 +282,42 @@ export interface UsePickerValueViewsResponse { setOpen: React.Dispatch>; } -/** - * Props passed to `usePickerLayoutProps`. - */ -export interface UsePickerValueLayoutResponse { - value: TValue; - onChange: (newValue: TValue) => void; - onSelectShortcut: ( - newValue: TValue, - changeImportance: PickerShortcutChangeImportance, - shortcut: PickersShortcutsItemContext, - ) => void; - isValid: (value: TValue) => boolean; -} - /** * Params passed to `usePickerProvider`. */ -export interface UsePickerValueProviderParams { +export interface UsePickerValueProviderParams { value: TValue; - contextValue: UsePickerValueContextValue; - actionsContextValue: UsePickerValueActionsContextValue; + contextValue: UsePickerValueContextValue; + actionsContextValue: UsePickerValueActionsContextValue; privateContextValue: UsePickerValuePrivateContextValue; + isValidContextValue: (value: TValue) => boolean; } export interface UsePickerValueResponse { viewProps: UsePickerValueViewsResponse; fieldProps: UsePickerValueFieldResponse; - layoutProps: UsePickerValueLayoutResponse; - provider: UsePickerValueProviderParams; + provider: UsePickerValueProviderParams; } -export interface UsePickerValueContextValue extends UsePickerValueActionsContextValue { +export interface UsePickerValueContextValue + extends UsePickerValueActionsContextValue { + /** + * The current value of the picker. + */ + value: TValue; /** * `true` if the picker is open, `false` otherwise. */ open: boolean; } -export interface UsePickerValueActionsContextValue { +export interface UsePickerValueActionsContextValue { + /** + * Set the current value of the picker. + * @param {TValue} value The new value of the picker. + * @param {SetValueActionOptions} options The options to customize the behavior of this update. + */ + setValue: (value: TValue, options?: SetValueActionOptions) => void; /** * Set the current open state of the Picker. * ```ts @@ -369,3 +359,23 @@ export interface UsePickerValuePrivateContextValue { */ dismissViews: () => void; } + +export interface SetValueActionOptions { + /** + * Importance of the change when picking a value: + * - "accept": fires `onChange`, fires `onAccept` and closes the picker. + * - "set": fires `onChange` but do not fire `onAccept` and does not close the picker. + * @default "accept" + */ + changeImportance?: PickerChangeImportance; + /** + * The validation error associated to the current value. + * If not defined, the validation will be re-applied by the picker. + */ + validationError?: TError; + /** + * The shortcut that triggered this change. + * Should not be defined if the change does not come from a shortcut. + */ + shortcut?: PickersShortcutsItemContext; +} diff --git a/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.tsx b/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.tsx index 0aee5be03535..4ff7b81044fb 100644 --- a/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.tsx @@ -30,12 +30,7 @@ export const useStaticPicker = < }: UseStaticPickerParams) => { const { localeText, slots, slotProps, className, sx, displayStaticWrapperAs, autoFocus } = props; - const { layoutProps, providerProps, renderCurrentView } = usePicker< - PickerValue, - TView, - TExternalProps, - {} - >({ + const { providerProps, renderCurrentView } = usePicker({ ...pickerParams, props, autoFocusView: autoFocus ?? false, @@ -50,7 +45,6 @@ export const useStaticPicker = < const renderPicker = () => ( - extends ExportedBaseToolbarProps { - isLandscape: boolean; - onChange: (newValue: TValue) => void; - value: TValue; +export interface BaseToolbarProps extends ExportedBaseToolbarProps { titleId?: string; } diff --git a/packages/x-date-pickers/src/models/pickers.ts b/packages/x-date-pickers/src/models/pickers.ts index 65f0bbc213ac..394abbc41e0c 100644 --- a/packages/x-date-pickers/src/models/pickers.ts +++ b/packages/x-date-pickers/src/models/pickers.ts @@ -16,6 +16,14 @@ export type PickerValidDate = keyof PickerValidDateLookup extends never ? any : PickerValidDateLookup[keyof PickerValidDateLookup]; +/** + * Importance of the change when picking a value: + * - "accept": fires `onChange`, fires `onAccept` and closes the picker. + * - "set": fires `onChange` but do not fire `onAccept` and does not close the picker. + * @default "accept" + */ +export type PickerChangeImportance = 'set' | 'accept'; + export interface PickerOwnerState { /** * `true` if the value of the picker is currently empty. diff --git a/packages/x-date-pickers/src/themeAugmentation/props.d.ts b/packages/x-date-pickers/src/themeAugmentation/props.d.ts index c630c3c86e2e..11562506ec71 100644 --- a/packages/x-date-pickers/src/themeAugmentation/props.d.ts +++ b/packages/x-date-pickers/src/themeAugmentation/props.d.ts @@ -76,7 +76,7 @@ export interface PickersComponentsPropsList { MuiPickersFadeTransitionGroup: PickersFadeTransitionGroupProps; MuiPickersPopper: PickerPopperProps; MuiPickersSlideTransition: ExportedSlideTransitionProps; - MuiPickersToolbar: PickersToolbarProps; + MuiPickersToolbar: PickersToolbarProps; MuiPickersToolbarButton: PickersToolbarButtonProps; MuiPickersToolbarText: ExportedPickersToolbarTextProps; MuiPickersLayout: PickersLayoutProps; diff --git a/scripts/x-date-pickers-pro.exports.json b/scripts/x-date-pickers-pro.exports.json index 4598d445e78f..997985d0f6b5 100644 --- a/scripts/x-date-pickers-pro.exports.json +++ b/scripts/x-date-pickers-pro.exports.json @@ -246,6 +246,7 @@ { "name": "NonEmptyDateRange", "kind": "TypeAlias" }, { "name": "OnErrorProps", "kind": "Interface" }, { "name": "PickerChangeHandlerContext", "kind": "Interface" }, + { "name": "PickerChangeImportance", "kind": "TypeAlias" }, { "name": "PickerDayOwnerState", "kind": "Interface" }, { "name": "PickerFieldSlotProps", "kind": "TypeAlias" }, { "name": "PickerLayoutOwnerState", "kind": "Interface" }, @@ -277,7 +278,6 @@ { "name": "PickersFilledInputClasses", "kind": "Interface" }, { "name": "PickersFilledInputClassKey", "kind": "TypeAlias" }, { "name": "PickersFilledInputProps", "kind": "Interface" }, - { "name": "PickerShortcutChangeImportance", "kind": "TypeAlias" }, { "name": "PickersInput", "kind": "Variable" }, { "name": "PickersInputBase", "kind": "Variable" }, { "name": "pickersInputBaseClasses", "kind": "Variable" }, @@ -408,6 +408,7 @@ { "name": "UseDateFieldProps", "kind": "Interface" }, { "name": "UseDateRangeFieldProps", "kind": "Interface" }, { "name": "UseDateTimeFieldProps", "kind": "Interface" }, + { "name": "useIsValidValue", "kind": "Function" }, { "name": "UseMultiInputDateRangeFieldComponentProps", "kind": "TypeAlias" }, { "name": "UseMultiInputDateRangeFieldProps", "kind": "Interface" }, { "name": "UseMultiInputDateTimeRangeFieldComponentProps", "kind": "TypeAlias" }, diff --git a/scripts/x-date-pickers.exports.json b/scripts/x-date-pickers.exports.json index b40281563702..86fcd590a518 100644 --- a/scripts/x-date-pickers.exports.json +++ b/scripts/x-date-pickers.exports.json @@ -161,6 +161,7 @@ { "name": "MultiSectionDigitalClockSlots", "kind": "Interface" }, { "name": "OnErrorProps", "kind": "Interface" }, { "name": "PickerChangeHandlerContext", "kind": "Interface" }, + { "name": "PickerChangeImportance", "kind": "TypeAlias" }, { "name": "PickerDayOwnerState", "kind": "Interface" }, { "name": "PickerFieldSlotProps", "kind": "TypeAlias" }, { "name": "PickerLayoutOwnerState", "kind": "Interface" }, @@ -191,7 +192,6 @@ { "name": "PickersFilledInputClasses", "kind": "Interface" }, { "name": "PickersFilledInputClassKey", "kind": "TypeAlias" }, { "name": "PickersFilledInputProps", "kind": "Interface" }, - { "name": "PickerShortcutChangeImportance", "kind": "TypeAlias" }, { "name": "PickersInput", "kind": "Variable" }, { "name": "PickersInputBase", "kind": "Variable" }, { "name": "pickersInputBaseClasses", "kind": "Variable" }, @@ -299,6 +299,7 @@ { "name": "UseClearableFieldSlots", "kind": "Interface" }, { "name": "UseDateFieldProps", "kind": "Interface" }, { "name": "UseDateTimeFieldProps", "kind": "Interface" }, + { "name": "useIsValidValue", "kind": "Function" }, { "name": "useParsedFormat", "kind": "Variable" }, { "name": "usePickerActionsContext", "kind": "Variable" }, { "name": "usePickerContext", "kind": "Variable" }, diff --git a/test/regressions/index.test.ts b/test/regressions/index.test.ts index 3f159c9bf6b5..6bac2dc16d1c 100644 --- a/test/regressions/index.test.ts +++ b/test/regressions/index.test.ts @@ -78,7 +78,7 @@ async function main() { await page.goto(`${baseUrl}#no-dev`, { waitUntil: 'networkidle' }); // Simulate portrait mode for date pickers. - // See `useIsLandscape`. + // See `usePickerOrientation`. await page.evaluate(() => { Object.defineProperty(window.screen.orientation, 'angle', { get() {