diff --git a/docs/data/date-pickers/experimentation/CustomField.js b/docs/data/date-pickers/experimentation/CustomField.js new file mode 100644 index 000000000000..3a093244e39a --- /dev/null +++ b/docs/data/date-pickers/experimentation/CustomField.js @@ -0,0 +1,47 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import TextField from '@mui/material/TextField'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { useValidation, validateDate } from '@mui/x-date-pickers/validation'; +import { useSplitFieldProps } from '@mui/x-date-pickers/hooks'; + +function ReadOnlyField(props) { + const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); + + const { value, timezone, format } = internalProps; + const { InputProps, slotProps, slots, ...other } = forwardedProps; + + const { hasValidationError } = useValidation({ + validator: validateDate, + value, + timezone, + props: internalProps, + }); + + return ( + + ); +} + +export default function CustomField() { + return ( + + + + + + ); +} diff --git a/docs/data/date-pickers/experimentation/CustomField.tsx b/docs/data/date-pickers/experimentation/CustomField.tsx new file mode 100644 index 000000000000..ddacbf62d264 --- /dev/null +++ b/docs/data/date-pickers/experimentation/CustomField.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import TextField from '@mui/material/TextField'; +import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { useValidation, validateDate } from '@mui/x-date-pickers/validation'; +import { useSplitFieldProps } from '@mui/x-date-pickers/hooks'; +import { DateFieldInPickerProps } from '@mui/x-date-pickers/DateField'; + +function ReadOnlyField(props: DateFieldInPickerProps) { + const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date'); + + const { value, timezone, format } = internalProps; + const { InputProps, slotProps, slots, ...other } = forwardedProps; + + const { hasValidationError } = useValidation({ + validator: validateDate, + value, + timezone, + props: internalProps, + }); + + return ( + + ); +} + +export default function CustomField() { + return ( + + + + + + ); +} diff --git a/docs/data/date-pickers/experimentation/CustomField.tsx.preview b/docs/data/date-pickers/experimentation/CustomField.tsx.preview new file mode 100644 index 000000000000..b489d70b2175 --- /dev/null +++ b/docs/data/date-pickers/experimentation/CustomField.tsx.preview @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/experimentation/experimentation.md b/docs/data/date-pickers/experimentation/experimentation.md new file mode 100644 index 000000000000..d5f906e32868 --- /dev/null +++ b/docs/data/date-pickers/experimentation/experimentation.md @@ -0,0 +1,11 @@ +--- +productId: x-date-pickers +--- + +# Date and Time Pickers experimentation + +

Demos not accessible through the navbar of the doc

+ +## Custom field + +{{"demo": "CustomField.js"}} diff --git a/docs/pages/x/react-date-pickers/experimentation.js b/docs/pages/x/react-date-pickers/experimentation.js new file mode 100644 index 000000000000..bf599b0bb69e --- /dev/null +++ b/docs/pages/x/react-date-pickers/experimentation.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docsx/data/date-pickers/experimentation/experimentation.md?muiMarkdown'; + +export default function Page() { + return ; +} diff --git a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx index f76e7d1ad76b..383e41d3a9e1 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { extractValidationProps, PickerViewRendererLookup } from '@mui/x-date-pickers/internals'; +import { PickerViewRendererLookup } from '@mui/x-date-pickers/internals'; +import { extractValidationProps } from '@mui/x-date-pickers/validation'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import resolveComponentProps from '@mui/utils/resolveComponentProps'; import { refType } from '@mui/utils'; @@ -10,7 +11,7 @@ import { useDateRangePickerDefaultizedProps } from '../DateRangePicker/shared'; import { renderDateRangeViewCalendar } from '../dateRangeViewRenderers'; import { MultiInputDateRangeField } from '../MultiInputDateRangeField'; import { useDesktopRangePicker } from '../internals/hooks/useDesktopRangePicker'; -import { validateDateRange } from '../internals/utils/validation/validateDateRange'; +import { validateDateRange } from '../validation'; import { DateRange } from '../models'; type DesktopDateRangePickerComponent = (< diff --git a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx index b25801087ea9..6c23460aac80 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { DefaultizedProps, - extractValidationProps, isDatePickerView, isInternalTimeView, PickerViewRenderer, @@ -10,6 +9,7 @@ import { resolveDateTimeFormat, useUtils, } from '@mui/x-date-pickers/internals'; +import { extractValidationProps } from '@mui/x-date-pickers/validation'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import resolveComponentProps from '@mui/utils/resolveComponentProps'; import { refType } from '@mui/utils'; @@ -32,7 +32,7 @@ import { useDesktopRangePicker, UseDesktopRangePickerProps, } from '../internals/hooks/useDesktopRangePicker'; -import { validateDateTimeRange } from '../internals/utils/validation/validateDateTimeRange'; +import { validateDateTimeRange } from '../validation'; import { DateTimeRangePickerView } from '../internals/models'; import { DateRange } from '../models'; import { diff --git a/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx b/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx index 6cb841389f50..a93c3eefd388 100644 --- a/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { extractValidationProps, PickerViewRendererLookup } from '@mui/x-date-pickers/internals'; +import { PickerViewRendererLookup } from '@mui/x-date-pickers/internals'; +import { extractValidationProps } from '@mui/x-date-pickers/validation'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import resolveComponentProps from '@mui/utils/resolveComponentProps'; import { refType } from '@mui/utils'; @@ -10,7 +11,7 @@ import { useDateRangePickerDefaultizedProps } from '../DateRangePicker/shared'; import { renderDateRangeViewCalendar } from '../dateRangeViewRenderers'; import { MultiInputDateRangeField } from '../MultiInputDateRangeField'; import { useMobileRangePicker } from '../internals/hooks/useMobileRangePicker'; -import { validateDateRange } from '../internals/utils/validation/validateDateRange'; +import { validateDateRange } from '../validation'; import { DateRange } from '../models'; type MobileDateRangePickerComponent = (< diff --git a/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx b/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx index 63637dbce374..42149a94852a 100644 --- a/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx @@ -4,7 +4,6 @@ import { refType } from '@mui/utils'; import { DIALOG_WIDTH, VIEW_HEIGHT, - extractValidationProps, isInternalTimeView, isDatePickerView, PickerViewRenderer, @@ -14,6 +13,7 @@ import { resolveDateTimeFormat, useUtils, } from '@mui/x-date-pickers/internals'; +import { extractValidationProps } from '@mui/x-date-pickers/validation'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import resolveComponentProps from '@mui/utils/resolveComponentProps'; import { @@ -32,7 +32,7 @@ import { UseMobileRangePickerProps, useMobileRangePicker, } from '../internals/hooks/useMobileRangePicker'; -import { validateDateTimeRange } from '../internals/utils/validation/validateDateTimeRange'; +import { validateDateTimeRange } from '../validation'; import { DateTimeRangePickerView } from '../internals/models'; import { DateRange } from '../models'; import { diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts index 06bcd2efd335..37a89e42ee95 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts @@ -4,7 +4,7 @@ import { useSplitFieldProps } from '@mui/x-date-pickers/hooks'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import { UseSingleInputDateRangeFieldProps } from './SingleInputDateRangeField.types'; import { rangeValueManager, getRangeFieldValueManager } from '../internals/utils/valueManagers'; -import { validateDateRange } from '../internals/utils/validation/validateDateRange'; +import { validateDateRange } from '../validation'; import { RangeFieldSection, DateRange } from '../models'; export const useSingleInputDateRangeField = < diff --git a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/useSingleInputDateTimeRangeField.ts b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/useSingleInputDateTimeRangeField.ts index 62356882aa58..3d42c8b318ad 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/useSingleInputDateTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/useSingleInputDateTimeRangeField.ts @@ -4,7 +4,7 @@ import { useSplitFieldProps } from '@mui/x-date-pickers/hooks'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import { UseSingleInputDateTimeRangeFieldProps } from './SingleInputDateTimeRangeField.types'; import { rangeValueManager, getRangeFieldValueManager } from '../internals/utils/valueManagers'; -import { validateDateTimeRange } from '../internals/utils/validation/validateDateTimeRange'; +import { validateDateTimeRange } from '../validation'; import { RangeFieldSection, DateRange } from '../models'; export const useSingleInputDateTimeRangeField = < diff --git a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/useSingleInputTimeRangeField.ts b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/useSingleInputTimeRangeField.ts index 2d5836c8882c..a60f71f889b3 100644 --- a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/useSingleInputTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/useSingleInputTimeRangeField.ts @@ -4,7 +4,7 @@ import { useSplitFieldProps } from '@mui/x-date-pickers/hooks'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import { UseSingleInputTimeRangeFieldProps } from './SingleInputTimeRangeField.types'; import { rangeValueManager, getRangeFieldValueManager } from '../internals/utils/valueManagers'; -import { validateTimeRange } from '../internals/utils/validation/validateTimeRange'; +import { validateTimeRange } from '../validation'; import { RangeFieldSection, DateRange } from '../models'; export const useSingleInputTimeRangeField = < diff --git a/packages/x-date-pickers-pro/src/StaticDateRangePicker/StaticDateRangePicker.tsx b/packages/x-date-pickers-pro/src/StaticDateRangePicker/StaticDateRangePicker.tsx index 5283f9efdac6..7f0cd35a8c68 100644 --- a/packages/x-date-pickers-pro/src/StaticDateRangePicker/StaticDateRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/StaticDateRangePicker/StaticDateRangePicker.tsx @@ -7,7 +7,7 @@ import { StaticDateRangePickerProps } from './StaticDateRangePicker.types'; import { useDateRangePickerDefaultizedProps } from '../DateRangePicker/shared'; import { renderDateRangeViewCalendar } from '../dateRangeViewRenderers'; import { rangeValueManager } from '../internals/utils/valueManagers'; -import { validateDateRange } from '../internals/utils/validation/validateDateRange'; +import { validateDateRange } from '../validation'; import { DateRange } from '../models'; type StaticDateRangePickerComponent = (( diff --git a/packages/x-date-pickers-pro/src/index.ts b/packages/x-date-pickers-pro/src/index.ts index 55114fb14fb8..8a7bb7e61c04 100644 --- a/packages/x-date-pickers-pro/src/index.ts +++ b/packages/x-date-pickers-pro/src/index.ts @@ -35,3 +35,4 @@ export * from './MobileDateTimeRangePicker'; export * from './dateRangeViewRenderers'; export * from './models'; +export * from './validation'; 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 cb635fdba2ca..55d56d9b6ca5 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 @@ -8,12 +8,16 @@ import { getActiveElement, usePicker, PickersPopper, - InferError, ExportedBaseToolbarProps, DateOrTimeViewWithMeridiem, ExportedBaseTabsProps, } from '@mui/x-date-pickers/internals'; -import { PickerValidDate, FieldRef, BaseSingleInputFieldProps } from '@mui/x-date-pickers/models'; +import { + PickerValidDate, + FieldRef, + BaseSingleInputFieldProps, + InferError, +} from '@mui/x-date-pickers/models'; import { DesktopRangePickerAdditionalViewProps, UseDesktopRangePickerParams, 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 8c0ff7404326..518914bdb1b5 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 @@ -6,13 +6,17 @@ import { PickersLayout, PickersLayoutSlotProps } from '@mui/x-date-pickers/Picke import { usePicker, PickersModalDialog, - InferError, ExportedBaseToolbarProps, DateOrTimeViewWithMeridiem, ExportedBaseTabsProps, } from '@mui/x-date-pickers/internals'; import { usePickersTranslations } from '@mui/x-date-pickers/hooks'; -import { PickerValidDate, FieldRef, BaseSingleInputFieldProps } from '@mui/x-date-pickers/models'; +import { + PickerValidDate, + FieldRef, + BaseSingleInputFieldProps, + InferError, +} from '@mui/x-date-pickers/models'; import useId from '@mui/utils/useId'; import { MobileRangePickerAdditionalViewProps, diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts index 84dfaeeb8631..f89396ebc68b 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts @@ -4,23 +4,19 @@ import { UseDateFieldComponentProps, } from '@mui/x-date-pickers/DateField'; import { - useLocalizationContext, - useValidation, FieldChangeHandler, FieldChangeHandlerContext, UseFieldResponse, useControlledValueWithTimezone, useDefaultizedDateField, } from '@mui/x-date-pickers/internals'; +import { useValidation } from '@mui/x-date-pickers/validation'; import { DateValidationError, PickerValidDate } from '@mui/x-date-pickers/models'; import { UseMultiInputDateRangeFieldParams, UseMultiInputDateRangeFieldProps, } from '../../../MultiInputDateRangeField/MultiInputDateRangeField.types'; -import { - DateRangeComponentValidationProps, - validateDateRange, -} from '../../utils/validation/validateDateRange'; +import { validateDateRange } from '../../../validation'; import { rangeValueManager } from '../../utils/valueManagers'; import type { UseMultiInputRangeFieldResponse } from './useMultiInputRangeField.types'; import { DateRangeValidationError, DateRange } from '../../../models'; @@ -48,8 +44,6 @@ export const useMultiInputDateRangeField = < typeof inSharedProps >(inSharedProps); - const adapter = useLocalizationContext(); - const { value: valueProp, defaultValue, @@ -75,6 +69,14 @@ export const useMultiInputDateRangeField = < valueManager: rangeValueManager, }); + const { validationError, getValidationErrorForNewValue } = useValidation({ + props: sharedProps, + value, + timezone, + validator: validateDateRange, + onError: sharedProps.onError, + }); + // TODO: Maybe export utility from `useField` instead of copy/pasting the logic const buildChangeHandler = ( index: 0 | 1, @@ -85,11 +87,7 @@ export const useMultiInputDateRangeField = < const context: FieldChangeHandlerContext = { ...rawContext, - validationError: validateDateRange({ - adapter, - value: newDateRange, - props: { ...sharedProps, timezone }, - }), + validationError: getValidationErrorForNewValue(newDateRange), }; handleValueChange(newDateRange, context); @@ -99,18 +97,6 @@ export const useMultiInputDateRangeField = < const handleStartDateChange = useEventCallback(buildChangeHandler(0)); const handleEndDateChange = useEventCallback(buildChangeHandler(1)); - const validationError = useValidation< - DateRange, - TDate, - DateRangeValidationError, - DateRangeComponentValidationProps - >( - { ...sharedProps, value, timezone }, - validateDateRange, - rangeValueManager.isSameError, - rangeValueManager.defaultErrorState, - ); - const selectedSectionsResponse = useMultiInputFieldSelectedSections({ selectedSections, onSelectedSectionsChange, diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts index cecc16b5cc6e..60efbe608a10 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts @@ -4,8 +4,6 @@ import { UseDateTimeFieldComponentProps, } from '@mui/x-date-pickers/DateTimeField'; import { - useLocalizationContext, - useValidation, FieldChangeHandler, FieldChangeHandlerContext, UseFieldResponse, @@ -13,15 +11,13 @@ import { useDefaultizedDateTimeField, } from '@mui/x-date-pickers/internals'; import { DateTimeValidationError, PickerValidDate } from '@mui/x-date-pickers/models'; +import { useValidation } from '@mui/x-date-pickers/validation'; import type { UseMultiInputDateTimeRangeFieldParams, UseMultiInputDateTimeRangeFieldProps, } from '../../../MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types'; import { DateTimeRangeValidationError, DateRange } from '../../../models'; -import { - DateTimeRangeComponentValidationProps, - validateDateTimeRange, -} from '../../utils/validation/validateDateTimeRange'; +import { validateDateTimeRange } from '../../../validation'; import { rangeValueManager } from '../../utils/valueManagers'; import type { UseMultiInputRangeFieldResponse } from './useMultiInputRangeField.types'; import { excludeProps } from './shared'; @@ -47,7 +43,6 @@ export const useMultiInputDateTimeRangeField = < UseMultiInputDateTimeRangeFieldProps, typeof inSharedProps >(inSharedProps); - const adapter = useLocalizationContext(); const { value: valueProp, @@ -74,6 +69,14 @@ export const useMultiInputDateTimeRangeField = < valueManager: rangeValueManager, }); + const { validationError, getValidationErrorForNewValue } = useValidation({ + props: sharedProps, + value, + timezone, + validator: validateDateTimeRange, + onError: sharedProps.onError, + }); + // TODO: Maybe export utility from `useField` instead of copy/pasting the logic const buildChangeHandler = ( index: 0 | 1, @@ -84,11 +87,7 @@ export const useMultiInputDateTimeRangeField = < const context: FieldChangeHandlerContext = { ...rawContext, - validationError: validateDateTimeRange({ - adapter, - value: newDateRange, - props: { ...sharedProps, timezone }, - }), + validationError: getValidationErrorForNewValue(newDateRange), }; handleValueChange(newDateRange, context); @@ -98,18 +97,6 @@ export const useMultiInputDateTimeRangeField = < const handleStartDateChange = useEventCallback(buildChangeHandler(0)); const handleEndDateChange = useEventCallback(buildChangeHandler(1)); - const validationError = useValidation< - DateRange, - TDate, - DateTimeRangeValidationError, - DateTimeRangeComponentValidationProps - >( - { ...sharedProps, value, timezone }, - validateDateTimeRange, - rangeValueManager.isSameError, - rangeValueManager.defaultErrorState, - ); - const selectedSectionsResponse = useMultiInputFieldSelectedSections({ selectedSections, onSelectedSectionsChange, diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts index a27dcb41a897..86db843894f5 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts @@ -4,19 +4,15 @@ import { UseTimeFieldComponentProps, } from '@mui/x-date-pickers/TimeField'; import { - useLocalizationContext, - useValidation, FieldChangeHandler, FieldChangeHandlerContext, UseFieldResponse, useControlledValueWithTimezone, useDefaultizedTimeField, } from '@mui/x-date-pickers/internals'; +import { useValidation } from '@mui/x-date-pickers/validation'; import { PickerValidDate, TimeValidationError } from '@mui/x-date-pickers/models'; -import { - validateTimeRange, - TimeRangeComponentValidationProps, -} from '../../utils/validation/validateTimeRange'; +import { validateTimeRange } from '../../../validation'; import { TimeRangeValidationError, DateRange } from '../../../models'; import type { UseMultiInputTimeRangeFieldParams, @@ -47,7 +43,6 @@ export const useMultiInputTimeRangeField = < UseMultiInputTimeRangeFieldProps, typeof inSharedProps >(inSharedProps); - const adapter = useLocalizationContext(); const { value: valueProp, @@ -74,6 +69,14 @@ export const useMultiInputTimeRangeField = < valueManager: rangeValueManager, }); + const { validationError, getValidationErrorForNewValue } = useValidation({ + props: sharedProps, + validator: validateTimeRange, + value, + timezone, + onError: sharedProps.onError, + }); + // TODO: Maybe export utility from `useField` instead of copy/pasting the logic const buildChangeHandler = ( index: 0 | 1, @@ -84,11 +87,7 @@ export const useMultiInputTimeRangeField = < const context: FieldChangeHandlerContext = { ...rawContext, - validationError: validateTimeRange({ - adapter, - value: newDateRange, - props: { ...sharedProps, timezone }, - }), + validationError: getValidationErrorForNewValue(newDateRange), }; handleValueChange(newDateRange, context); @@ -98,18 +97,6 @@ export const useMultiInputTimeRangeField = < const handleStartDateChange = useEventCallback(buildChangeHandler(0)); const handleEndDateChange = useEventCallback(buildChangeHandler(1)); - const validationError = useValidation< - DateRange, - TDate, - TimeRangeValidationError, - TimeRangeComponentValidationProps - >( - { ...sharedProps, value, timezone }, - validateTimeRange, - rangeValueManager.isSameError, - rangeValueManager.defaultErrorState, - ); - const selectedSectionsResponse = useMultiInputFieldSelectedSections({ selectedSections, onSelectedSectionsChange, diff --git a/packages/x-date-pickers-pro/src/validation/index.ts b/packages/x-date-pickers-pro/src/validation/index.ts new file mode 100644 index 000000000000..9b7737a17ed2 --- /dev/null +++ b/packages/x-date-pickers-pro/src/validation/index.ts @@ -0,0 +1,8 @@ +export { validateDateRange } from './validateDateRange'; +export type { ValidateDateRangeProps } from './validateDateRange'; + +export { validateTimeRange } from './validateTimeRange'; +export type { ValidateTimeRangeProps } from './validateTimeRange'; + +export { validateDateTimeRange } from './validateDateTimeRange'; +export type { ValidateDateTimeRangeProps } from './validateDateTimeRange'; diff --git a/packages/x-date-pickers-pro/src/internals/utils/validation/validateDateRange.ts b/packages/x-date-pickers-pro/src/validation/validateDateRange.ts similarity index 55% rename from packages/x-date-pickers-pro/src/internals/utils/validation/validateDateRange.ts rename to packages/x-date-pickers-pro/src/validation/validateDateRange.ts index bb138c82f438..125449ca9a6f 100644 --- a/packages/x-date-pickers-pro/src/internals/utils/validation/validateDateRange.ts +++ b/packages/x-date-pickers-pro/src/validation/validateDateRange.ts @@ -1,25 +1,21 @@ -import { PickerValidDate, TimezoneProps } from '@mui/x-date-pickers/models'; -import { - Validator, - validateDate, - BaseDateValidationProps, - DefaultizedProps, -} from '@mui/x-date-pickers/internals'; -import { isRangeValid } from '../date-utils'; -import { DayRangeValidationProps } from '../../models/dateRange'; -import { DateRangeValidationError, DateRange } from '../../../models'; - -export interface DateRangeComponentValidationProps +import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { validateDate, Validator } from '@mui/x-date-pickers/validation'; +import { BaseDateValidationProps } from '@mui/x-date-pickers/internals'; +import { isRangeValid } from '../internals/utils/date-utils'; +import { DayRangeValidationProps } from '../internals/models/dateRange'; +import { DateRangeValidationError, DateRange } from '../models'; +import { rangeValueManager } from '../internals/utils/valueManagers'; + +export interface ValidateDateRangeProps extends DayRangeValidationProps, - Required>, - DefaultizedProps {} + Required> {} export const validateDateRange: Validator< DateRange, any, DateRangeValidationError, - DateRangeComponentValidationProps -> = ({ props, value, adapter }) => { + ValidateDateRangeProps +> = ({ adapter, value, timezone, props }) => { const [start, end] = value; const { shouldDisableDate, ...otherProps } = props; @@ -28,6 +24,7 @@ export const validateDateRange: Validator< validateDate({ adapter, value: start, + timezone, props: { ...otherProps, shouldDisableDate: (day) => !!shouldDisableDate?.(day, 'start'), @@ -36,6 +33,7 @@ export const validateDateRange: Validator< validateDate({ adapter, value: end, + timezone, props: { ...otherProps, shouldDisableDate: (day) => !!shouldDisableDate?.(day, 'end'), @@ -58,3 +56,5 @@ export const validateDateRange: Validator< return [null, null]; }; + +validateDateRange.valueManager = rangeValueManager; diff --git a/packages/x-date-pickers-pro/src/internals/utils/validation/validateDateTimeRange.ts b/packages/x-date-pickers-pro/src/validation/validateDateTimeRange.ts similarity index 55% rename from packages/x-date-pickers-pro/src/internals/utils/validation/validateDateTimeRange.ts rename to packages/x-date-pickers-pro/src/validation/validateDateTimeRange.ts index 2eae3041a2e4..d86d65e8d3b3 100644 --- a/packages/x-date-pickers-pro/src/internals/utils/validation/validateDateTimeRange.ts +++ b/packages/x-date-pickers-pro/src/validation/validateDateTimeRange.ts @@ -1,27 +1,22 @@ -import { PickerValidDate, TimezoneProps } from '@mui/x-date-pickers/models'; -import { - Validator, - validateDateTime, - BaseDateValidationProps, - TimeValidationProps, - DefaultizedProps, -} from '@mui/x-date-pickers/internals'; -import { isRangeValid } from '../date-utils'; -import { DayRangeValidationProps } from '../../models/dateRange'; -import { DateTimeRangeValidationError, DateRange } from '../../../models'; - -export interface DateTimeRangeComponentValidationProps +import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { validateDateTime, Validator } from '@mui/x-date-pickers/validation'; +import { BaseDateValidationProps, TimeValidationProps } from '@mui/x-date-pickers/internals'; +import { isRangeValid } from '../internals/utils/date-utils'; +import { DayRangeValidationProps } from '../internals/models/dateRange'; +import { DateTimeRangeValidationError, DateRange } from '../models'; +import { rangeValueManager } from '../internals/utils/valueManagers'; + +export interface ValidateDateTimeRangeProps extends DayRangeValidationProps, TimeValidationProps, - Required>, - DefaultizedProps {} + Required> {} export const validateDateTimeRange: Validator< DateRange, any, DateTimeRangeValidationError, - DateTimeRangeComponentValidationProps -> = ({ props, value, adapter }) => { + ValidateDateTimeRangeProps +> = ({ adapter, value, timezone, props }) => { const [start, end] = value; const { shouldDisableDate, ...otherProps } = props; @@ -30,6 +25,7 @@ export const validateDateTimeRange: Validator< validateDateTime({ adapter, value: start, + timezone, props: { ...otherProps, shouldDisableDate: (day) => !!shouldDisableDate?.(day, 'start'), @@ -38,6 +34,7 @@ export const validateDateTimeRange: Validator< validateDateTime({ adapter, value: end, + timezone, props: { ...otherProps, shouldDisableDate: (day) => !!shouldDisableDate?.(day, 'end'), @@ -60,3 +57,5 @@ export const validateDateTimeRange: Validator< return [null, null]; }; + +validateDateTimeRange.valueManager = rangeValueManager; diff --git a/packages/x-date-pickers-pro/src/internals/utils/validation/validateTimeRange.ts b/packages/x-date-pickers-pro/src/validation/validateTimeRange.ts similarity index 52% rename from packages/x-date-pickers-pro/src/internals/utils/validation/validateTimeRange.ts rename to packages/x-date-pickers-pro/src/validation/validateTimeRange.ts index 15b4a2bc2cd6..483523a4b499 100644 --- a/packages/x-date-pickers-pro/src/internals/utils/validation/validateTimeRange.ts +++ b/packages/x-date-pickers-pro/src/validation/validateTimeRange.ts @@ -1,34 +1,30 @@ -import { TimezoneProps } from '@mui/x-date-pickers/models'; -import { - Validator, - validateTime, - BaseTimeValidationProps, - DefaultizedProps, -} from '@mui/x-date-pickers/internals'; -import { isRangeValid } from '../date-utils'; -import { TimeRangeValidationError, DateRange } from '../../../models'; - -export interface TimeRangeComponentValidationProps - extends Required, - DefaultizedProps {} +import { validateTime, Validator } from '@mui/x-date-pickers/validation'; +import { BaseTimeValidationProps } from '@mui/x-date-pickers/internals'; +import { isRangeValid } from '../internals/utils/date-utils'; +import { TimeRangeValidationError, DateRange } from '../models'; +import { rangeValueManager } from '../internals/utils/valueManagers'; + +export interface ValidateTimeRangeProps extends Required {} export const validateTimeRange: Validator< DateRange, any, TimeRangeValidationError, - TimeRangeComponentValidationProps -> = ({ props, value, adapter }) => { + ValidateTimeRangeProps +> = ({ adapter, value, timezone, props }) => { const [start, end] = value; const dateTimeValidations: TimeRangeValidationError = [ validateTime({ adapter, value: start, + timezone, props, }), validateTime({ adapter, value: end, + timezone, props, }), ]; @@ -48,3 +44,5 @@ export const validateTimeRange: Validator< return [null, null]; }; + +validateTimeRange.valueManager = rangeValueManager; diff --git a/packages/x-date-pickers/src/DateCalendar/useIsDateDisabled.ts b/packages/x-date-pickers/src/DateCalendar/useIsDateDisabled.ts index 7fbde8e84772..2c677f6f7653 100644 --- a/packages/x-date-pickers/src/DateCalendar/useIsDateDisabled.ts +++ b/packages/x-date-pickers/src/DateCalendar/useIsDateDisabled.ts @@ -1,10 +1,8 @@ import * as React from 'react'; -import { - DateComponentValidationProps, - validateDate, -} from '../internals/utils/validation/validateDate'; +import { ValidateDateProps, validateDate } from '../validation'; import { useLocalizationContext } from '../internals/hooks/useUtils'; -import { PickerValidDate } from '../models'; +import { PickerValidDate, TimezoneProps } from '../models'; +import { DefaultizedProps } from '../internals/models/helpers'; export const useIsDateDisabled = ({ shouldDisableDate, @@ -15,7 +13,7 @@ export const useIsDateDisabled = ({ disableFuture, disablePast, timezone, -}: DateComponentValidationProps) => { +}: ValidateDateProps & DefaultizedProps) => { const adapter = useLocalizationContext(); return React.useCallback( @@ -23,6 +21,7 @@ export const useIsDateDisabled = ({ validateDate({ adapter, value: day, + timezone, props: { shouldDisableDate, shouldDisableMonth, @@ -31,7 +30,6 @@ export const useIsDateDisabled = ({ maxDate, disableFuture, disablePast, - timezone, }, }) !== null, [ diff --git a/packages/x-date-pickers/src/DateField/DateField.types.ts b/packages/x-date-pickers/src/DateField/DateField.types.ts index cf1bdfcc800a..57ab1e1830ae 100644 --- a/packages/x-date-pickers/src/DateField/DateField.types.ts +++ b/packages/x-date-pickers/src/DateField/DateField.types.ts @@ -11,9 +11,10 @@ import { FieldSection, PickerValidDate, BuiltInFieldTextFieldProps, + BaseSingleInputFieldProps, } from '../models'; import { UseFieldInternalProps } from '../internals/hooks/useField'; -import { MakeOptional } from '../internals/models/helpers'; +import { MakeOptional, DefaultizedProps } from '../internals/models/helpers'; import { BaseDateValidationProps, DayValidationProps, @@ -40,6 +41,19 @@ export interface UseDateFieldProps< BaseDateValidationProps, ExportedUseClearableFieldProps {} +/** + * Props the field can receive when used inside a date picker. + * (`DatePicker`, `DesktopDatePicker` or `MobileDatePicker` component). + */ +export type DateFieldInPickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> = DefaultizedProps< + UseDateFieldProps, + 'format' | 'timezone' | keyof BaseDateValidationProps +> & + BaseSingleInputFieldProps; + export type UseDateFieldComponentProps< TDate extends PickerValidDate, TEnableAccessibleFieldDOMStructure extends boolean, diff --git a/packages/x-date-pickers/src/DateField/index.ts b/packages/x-date-pickers/src/DateField/index.ts index cd6119e98a25..e5f8fe21dcb2 100644 --- a/packages/x-date-pickers/src/DateField/index.ts +++ b/packages/x-date-pickers/src/DateField/index.ts @@ -4,4 +4,5 @@ export type { UseDateFieldProps, UseDateFieldComponentProps, DateFieldProps, + DateFieldInPickerProps, } from './DateField.types'; diff --git a/packages/x-date-pickers/src/DateField/useDateField.ts b/packages/x-date-pickers/src/DateField/useDateField.ts index ff4404d47421..4d7fe61a702a 100644 --- a/packages/x-date-pickers/src/DateField/useDateField.ts +++ b/packages/x-date-pickers/src/DateField/useDateField.ts @@ -4,7 +4,7 @@ import { } from '../internals/utils/valueManagers'; import { useField } from '../internals/hooks/useField'; import { UseDateFieldProps } from './DateField.types'; -import { validateDate } from '../internals/utils/validation/validateDate'; +import { validateDate } from '../validation'; import { useSplitFieldProps } from '../hooks'; import { FieldSection, PickerValidDate } from '../models'; import { useDefaultizedDateField } from '../internals/hooks/defaultizedFieldProps'; diff --git a/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts b/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts index 625f70532e55..e19e41a6345a 100644 --- a/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts +++ b/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts @@ -6,9 +6,10 @@ import { FieldSection, PickerValidDate, BuiltInFieldTextFieldProps, + BaseSingleInputFieldProps, } from '../models'; import { UseFieldInternalProps } from '../internals/hooks/useField'; -import { MakeOptional } from '../internals/models/helpers'; +import { DefaultizedProps, MakeOptional } from '../internals/models/helpers'; import { BaseDateValidationProps, BaseTimeValidationProps, @@ -52,6 +53,23 @@ export interface UseDateTimeFieldProps< ampm?: boolean; } +/** + * Props the field can receive when used inside a date time picker. + * (`DateTimePicker`, `DesktopDateTimePicker` or `MobileDateTimePicker` component). + */ +export type DateTimeFieldInPickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> = DefaultizedProps< + UseDateTimeFieldProps, + | 'format' + | 'timezone' + | 'ampm' + | keyof BaseDateValidationProps + | keyof BaseTimeValidationProps +> & + BaseSingleInputFieldProps; + export type UseDateTimeFieldComponentProps< TDate extends PickerValidDate, TEnableAccessibleFieldDOMStructure extends boolean, diff --git a/packages/x-date-pickers/src/DateTimeField/index.ts b/packages/x-date-pickers/src/DateTimeField/index.ts index 95952dde9474..212ce42432f5 100644 --- a/packages/x-date-pickers/src/DateTimeField/index.ts +++ b/packages/x-date-pickers/src/DateTimeField/index.ts @@ -4,4 +4,5 @@ export type { UseDateTimeFieldProps, UseDateTimeFieldComponentProps, DateTimeFieldProps, + DateTimeFieldInPickerProps, } from './DateTimeField.types'; diff --git a/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts b/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts index 3b698f37a133..bcdc1c92b745 100644 --- a/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts +++ b/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts @@ -4,7 +4,7 @@ import { } from '../internals/utils/valueManagers'; import { useField } from '../internals/hooks/useField'; import { UseDateTimeFieldProps } from './DateTimeField.types'; -import { validateDateTime } from '../internals/utils/validation/validateDateTime'; +import { validateDateTime } from '../validation'; import { useSplitFieldProps } from '../hooks'; import { FieldSection, PickerValidDate } from '../models'; import { useDefaultizedDateTimeField } from '../internals/hooks/defaultizedFieldProps'; diff --git a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx index fcea15c593a7..ca78737d2675 100644 --- a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx +++ b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx @@ -7,12 +7,11 @@ import { DesktopDatePickerProps } from './DesktopDatePicker.types'; import { DatePickerViewRenderers, useDatePickerDefaultizedProps } from '../DatePicker/shared'; import { usePickersTranslations } from '../hooks/usePickersTranslations'; import { useUtils } from '../internals/hooks/useUtils'; -import { validateDate } from '../internals/utils/validation/validateDate'; +import { validateDate, extractValidationProps } from '../validation'; import { DateView, PickerValidDate } from '../models'; import { useDesktopPicker } from '../internals/hooks/useDesktopPicker'; import { CalendarIcon } from '../icons'; import { DateField } from '../DateField'; -import { extractValidationProps } from '../internals/utils/validation/extractValidationProps'; import { renderDateViewCalendar } from '../dateViewRenderers'; import { resolveDateFormat } from '../internals/utils/date-utils'; import { buildGetOpenDialogAriaText } from '../locales/utils/getPickersLocalization'; diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx index 259a84364176..605c07e6fca4 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx @@ -13,11 +13,10 @@ import { import { renderDateViewCalendar } from '../dateViewRenderers/dateViewRenderers'; import { usePickersTranslations } from '../hooks/usePickersTranslations'; import { useUtils } from '../internals/hooks/useUtils'; -import { validateDateTime } from '../internals/utils/validation/validateDateTime'; +import { validateDateTime, extractValidationProps } from '../validation'; import { DateOrTimeViewWithMeridiem } from '../internals/models'; import { CalendarIcon } from '../icons'; import { UseDesktopPickerProps, useDesktopPicker } from '../internals/hooks/useDesktopPicker'; -import { extractValidationProps } from '../internals/utils/validation/extractValidationProps'; import { PickerViewsRendererProps } from '../internals/hooks/usePicker'; import { resolveDateTimeFormat, diff --git a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx index 42919188531c..d9c0436dd47d 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx @@ -8,10 +8,9 @@ import { DesktopTimePickerProps } from './DesktopTimePicker.types'; import { TimePickerViewRenderers, useTimePickerDefaultizedProps } from '../TimePicker/shared'; import { usePickersTranslations } from '../hooks/usePickersTranslations'; import { useUtils } from '../internals/hooks/useUtils'; -import { validateTime } from '../internals/utils/validation/validateTime'; +import { extractValidationProps, validateTime } from '../validation'; import { ClockIcon } from '../icons'; import { useDesktopPicker } from '../internals/hooks/useDesktopPicker'; -import { extractValidationProps } from '../internals/utils/validation/extractValidationProps'; import { renderDigitalClockTimeView, renderMultiSectionDigitalClockTimeView, diff --git a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx index 34fece6808b9..63d582d95c90 100644 --- a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx +++ b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx @@ -7,10 +7,9 @@ import { MobileDatePickerProps } from './MobileDatePicker.types'; import { DatePickerViewRenderers, useDatePickerDefaultizedProps } from '../DatePicker/shared'; import { usePickersTranslations } from '../hooks/usePickersTranslations'; import { useUtils } from '../internals/hooks/useUtils'; -import { validateDate } from '../internals/utils/validation/validateDate'; +import { extractValidationProps, validateDate } from '../validation'; import { DateView, PickerValidDate } from '../models'; import { DateField } from '../DateField'; -import { extractValidationProps } from '../internals/utils/validation/extractValidationProps'; import { singleItemValueManager } from '../internals/utils/valueManagers'; import { renderDateViewCalendar } from '../dateViewRenderers'; import { resolveDateFormat } from '../internals/utils/date-utils'; diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx index b04083666c4e..61f00976f527 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx @@ -11,10 +11,9 @@ import { } from '../DateTimePicker/shared'; import { usePickersTranslations } from '../hooks/usePickersTranslations'; import { useUtils } from '../internals/hooks/useUtils'; -import { validateDateTime } from '../internals/utils/validation/validateDateTime'; +import { extractValidationProps, validateDateTime } from '../validation'; import { DateOrTimeView, PickerValidDate } from '../models'; import { useMobilePicker } from '../internals/hooks/useMobilePicker'; -import { extractValidationProps } from '../internals/utils/validation/extractValidationProps'; import { renderDateViewCalendar } from '../dateViewRenderers'; import { renderTimeViewClock } from '../timeViewRenderers'; import { resolveDateTimeFormat } from '../internals/utils/date-time-utils'; diff --git a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx index 7a398cc43d1a..1577e1ea0fd6 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx @@ -8,10 +8,9 @@ import { MobileTimePickerProps } from './MobileTimePicker.types'; import { TimePickerViewRenderers, useTimePickerDefaultizedProps } from '../TimePicker/shared'; import { usePickersTranslations } from '../hooks/usePickersTranslations'; import { useUtils } from '../internals/hooks/useUtils'; -import { validateTime } from '../internals/utils/validation/validateTime'; +import { extractValidationProps, validateTime } from '../validation'; import { PickerValidDate, TimeView } from '../models'; import { useMobilePicker } from '../internals/hooks/useMobilePicker'; -import { extractValidationProps } from '../internals/utils/validation/extractValidationProps'; import { renderTimeViewClock } from '../timeViewRenderers'; import { resolveTimeFormat } from '../internals/utils/time-utils'; import { buildGetOpenDialogAriaText } from '../locales/utils/getPickersLocalization'; diff --git a/packages/x-date-pickers/src/StaticDatePicker/StaticDatePicker.tsx b/packages/x-date-pickers/src/StaticDatePicker/StaticDatePicker.tsx index 4338718c441b..46e6308aba4d 100644 --- a/packages/x-date-pickers/src/StaticDatePicker/StaticDatePicker.tsx +++ b/packages/x-date-pickers/src/StaticDatePicker/StaticDatePicker.tsx @@ -4,7 +4,7 @@ import { StaticDatePickerProps } from './StaticDatePicker.types'; import { DatePickerViewRenderers, useDatePickerDefaultizedProps } from '../DatePicker/shared'; import { renderDateViewCalendar } from '../dateViewRenderers'; import { useStaticPicker } from '../internals/hooks/useStaticPicker'; -import { validateDate } from '../internals/utils/validation/validateDate'; +import { validateDate } from '../validation'; import { DateView, PickerValidDate } from '../models'; import { singleItemValueManager } from '../internals/utils/valueManagers'; diff --git a/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.tsx b/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.tsx index a93916d40fcd..f1f7a5005e49 100644 --- a/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.tsx +++ b/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.tsx @@ -10,7 +10,7 @@ import { renderDateViewCalendar } from '../dateViewRenderers'; import { singleItemValueManager } from '../internals/utils/valueManagers'; import { useStaticPicker } from '../internals/hooks/useStaticPicker'; import { DateOrTimeView, PickerValidDate } from '../models'; -import { validateDateTime } from '../internals/utils/validation/validateDateTime'; +import { validateDateTime } from '../validation'; type StaticDateTimePickerComponent = (( props: StaticDateTimePickerProps & React.RefAttributes, diff --git a/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.tsx b/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.tsx index 49466bcc2123..31a54d09e0db 100644 --- a/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.tsx +++ b/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.tsx @@ -6,7 +6,7 @@ import { TimePickerViewRenderers, useTimePickerDefaultizedProps } from '../TimeP import { renderTimeViewClock } from '../timeViewRenderers'; import { singleItemValueManager } from '../internals/utils/valueManagers'; import { useStaticPicker } from '../internals/hooks/useStaticPicker'; -import { validateTime } from '../internals/utils/validation/validateTime'; +import { validateTime } from '../validation'; type StaticTimePickerComponent = (( props: StaticTimePickerProps & React.RefAttributes, diff --git a/packages/x-date-pickers/src/TimeField/TimeField.types.ts b/packages/x-date-pickers/src/TimeField/TimeField.types.ts index 478cf08ee9ee..0ce1a53d6e40 100644 --- a/packages/x-date-pickers/src/TimeField/TimeField.types.ts +++ b/packages/x-date-pickers/src/TimeField/TimeField.types.ts @@ -2,13 +2,14 @@ import * as React from 'react'; import { SlotComponentProps } from '@mui/utils'; import TextField from '@mui/material/TextField'; import { UseFieldInternalProps } from '../internals/hooks/useField'; -import { MakeOptional } from '../internals/models/helpers'; +import { DefaultizedProps, MakeOptional } from '../internals/models/helpers'; import { BaseTimeValidationProps, TimeValidationProps } from '../internals/models/validation'; import { FieldSection, PickerValidDate, TimeValidationError, BuiltInFieldTextFieldProps, + BaseSingleInputFieldProps, } from '../models'; import { ExportedUseClearableFieldProps, @@ -39,6 +40,19 @@ export interface UseTimeFieldProps< ampm?: boolean; } +/** + * Props the field can receive when used inside a time picker. + * (`TimePicker`, `DesktopTimePicker` or `MobileTimePicker` component). + */ +export type TimeFieldInPickerProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, +> = DefaultizedProps< + UseTimeFieldProps, + 'format' | 'timezone' | 'ampm' | keyof BaseTimeValidationProps +> & + BaseSingleInputFieldProps; + export type UseTimeFieldComponentProps< TDate extends PickerValidDate, TEnableAccessibleFieldDOMStructure extends boolean, diff --git a/packages/x-date-pickers/src/TimeField/index.ts b/packages/x-date-pickers/src/TimeField/index.ts index f335f0f8fd76..cf9c1e6a5881 100644 --- a/packages/x-date-pickers/src/TimeField/index.ts +++ b/packages/x-date-pickers/src/TimeField/index.ts @@ -4,4 +4,5 @@ export type { UseTimeFieldProps, UseTimeFieldComponentProps, TimeFieldProps, + TimeFieldInPickerProps, } from './TimeField.types'; diff --git a/packages/x-date-pickers/src/TimeField/useTimeField.ts b/packages/x-date-pickers/src/TimeField/useTimeField.ts index e3f97c8b4b5c..db4d9aa859df 100644 --- a/packages/x-date-pickers/src/TimeField/useTimeField.ts +++ b/packages/x-date-pickers/src/TimeField/useTimeField.ts @@ -4,7 +4,7 @@ import { } from '../internals/utils/valueManagers'; import { useField } from '../internals/hooks/useField'; import { UseTimeFieldProps } from './TimeField.types'; -import { validateTime } from '../internals/utils/validation/validateTime'; +import { validateTime } from '../validation'; import { useSplitFieldProps } from '../hooks'; import { PickerValidDate, FieldSection } from '../models'; import { useDefaultizedTimeField } from '../internals/hooks/defaultizedFieldProps'; diff --git a/packages/x-date-pickers/src/hooks/useSplitFieldProps.ts b/packages/x-date-pickers/src/hooks/useSplitFieldProps.ts index 150823985f69..3498bbf38df1 100644 --- a/packages/x-date-pickers/src/hooks/useSplitFieldProps.ts +++ b/packages/x-date-pickers/src/hooks/useSplitFieldProps.ts @@ -4,7 +4,7 @@ import { DATE_TIME_VALIDATION_PROP_NAMES, DATE_VALIDATION_PROP_NAMES, TIME_VALIDATION_PROP_NAMES, -} from '../internals/utils/validation/extractValidationProps'; +} from '../validation/extractValidationProps'; const SHARED_FIELD_INTERNAL_PROP_NAMES = [ 'value', diff --git a/packages/x-date-pickers/src/index.ts b/packages/x-date-pickers/src/index.ts index cd2e4e9586e5..f0987002adb1 100644 --- a/packages/x-date-pickers/src/index.ts +++ b/packages/x-date-pickers/src/index.ts @@ -53,7 +53,6 @@ export * from './PickersSectionList'; export { DEFAULT_DESKTOP_MODE_MEDIA_QUERY } from './internals/utils/utils'; export * from './models'; - export * from './icons'; - export * from './hooks'; +export * from './validation'; 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 97fb108f8733..b13a78700e27 100644 --- a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx @@ -14,12 +14,12 @@ import { import { usePicker } from '../usePicker'; import { LocalizationProvider } from '../../../LocalizationProvider'; import { PickersLayout } from '../../../PickersLayout'; -import { InferError } from '../useValidation'; import { FieldSection, PickerValidDate, FieldRef, BaseSingleInputFieldProps, + InferError, } from '../../../models'; import { DateOrTimeViewWithMeridiem } from '../../models'; diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.ts index 1d8bc5fdcffb..2ad8363212a9 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useField.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import useEventCallback from '@mui/utils/useEventCallback'; import { useRtl } from '@mui/system/RtlProvider'; -import { useValidation } from '../useValidation'; +import { useValidation } from '../../../validation'; import { useUtils } from '../useUtils'; import { UseFieldParams, @@ -226,12 +226,13 @@ export const useField = < interactions.syncSelectionToDOM(); }); - const validationError = useValidation( - { ...internalProps, value: state.value, timezone }, + const { hasValidationError } = useValidation({ + props: internalProps, validator, - valueManager.isSameError, - valueManager.defaultErrorState, - ); + timezone, + value: state.value, + onError: internalProps.onError, + }); const inputError = React.useMemo(() => { // only override when `error` is undefined. @@ -240,8 +241,8 @@ export const useField = < return error; } - return valueManager.hasError(validationError); - }, [valueManager, validationError, error]); + return hasValidationError; + }, [hasValidationError, error]); React.useEffect(() => { if (!inputError && activeSectionIndex == null) { diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts index 7700c06357b6..e558a7b3b7ee 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts @@ -7,12 +7,13 @@ import { TimezoneProps, FieldSectionContentType, FieldValueType, - PickersTimezone, PickerValidDate, FieldRef, + OnErrorProps, + InferError, } from '../../../models'; import type { PickerValueManager } from '../usePicker'; -import { InferError, Validator } from '../useValidation'; +import type { Validator } from '../../../validation'; import type { UseFieldStateResponse } from './useFieldState'; import type { UseFieldCharacterEditingResponse } from './useFieldCharacterEditing'; import { PickersSectionElement, PickersSectionListRef } from '../../../PickersSectionList'; @@ -37,12 +38,7 @@ export interface UseFieldParams< internalProps: TInternalProps; valueManager: PickerValueManager>; fieldValueManager: FieldValueManager; - validator: Validator< - TValue, - TDate, - InferError, - UseFieldValidationProps - >; + validator: Validator, TInternalProps>; valueType: FieldValueType; } @@ -52,7 +48,8 @@ export interface UseFieldInternalProps< TSection extends FieldSection, TEnableAccessibleFieldDOMStructure extends boolean, TError, -> extends TimezoneProps { +> extends TimezoneProps, + OnErrorProps { /** * The selected value. * Used when the component is controlled. @@ -76,16 +73,6 @@ export interface UseFieldInternalProps< * @param {FieldChangeHandlerContext} context The context containing the validation result of the current value. */ onChange?: FieldChangeHandler; - /** - * Callback fired when the error associated with the current value changes. - * When a validation error is detected, the `error` parameter contains a non-null value. - * This can be used to render an appropriate form error. - * @template TError The validation error type. It will be either `string` or a `null`. It can be in `[start, end]` format in case of range value. - * @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value. - * @param {TError} error The reason why the current value is not valid. - * @param {TValue} value The value associated with the error. - */ - onError?: (error: TError, value: TValue) => void; /** * Format of the date when rendered in the input(s). */ @@ -398,14 +385,6 @@ export interface UseFieldState { tempValueStrAndroid: string | null; } -export type UseFieldValidationProps< - TValue, - TInternalProps extends { value?: TValue; defaultValue?: TValue; timezone?: PickersTimezone }, -> = Omit & { - value: TValue; - timezone: PickersTimezone; -}; - export type AvailableAdjustKeyCode = | 'ArrowUp' | 'ArrowDown' diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts b/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts index 23e6bbc66900..846ab4be676d 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts @@ -21,12 +21,12 @@ import { getLocalizedDigits, } from './useField.utils'; import { buildSectionsFromFormat } from './buildSectionsFromFormat'; -import { InferError } from '../useValidation'; import { FieldSection, FieldSelectedSections, PickersTimezone, PickerValidDate, + InferError, } from '../../../models'; import { useValueWithTimezone } from '../useValueWithTimezone'; import { @@ -228,7 +228,8 @@ export const useFieldState = < validationError: validator({ adapter, value, - props: { ...internalProps, value, timezone }, + timezone, + props: internalProps, }), }; 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 15b31e4fc63a..6145d21537ae 100644 --- a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx @@ -12,12 +12,12 @@ import { usePicker } from '../usePicker'; import { onSpaceOrEnter } from '../../utils/utils'; import { LocalizationProvider } from '../../../LocalizationProvider'; import { PickersLayout } from '../../../PickersLayout'; -import { InferError } from '../useValidation'; import { FieldSection, BaseSingleInputFieldProps, PickerValidDate, FieldRef, + InferError, } from '../../../models'; import { DateOrTimeViewWithMeridiem } from '../../models'; 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 4ac218f211b0..8c74daccc437 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts @@ -3,8 +3,7 @@ import { UsePickerParams, UsePickerProps, UsePickerResponse } from './usePicker. import { usePickerValue } from './usePickerValue'; import { usePickerViews } from './usePickerViews'; import { usePickerLayoutProps } from './usePickerLayoutProps'; -import { InferError } from '../useValidation'; -import { FieldSection, PickerValidDate } from '../../../models'; +import { FieldSection, PickerValidDate, InferError } from '../../../models'; import { DateOrTimeViewWithMeridiem } from '../../models'; export const usePicker = < 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 3bd7a5eb08a2..8d6255b5f562 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts @@ -3,8 +3,13 @@ import useEventCallback from '@mui/utils/useEventCallback'; import { useOpenState } from '../useOpenState'; import { useLocalizationContext, useUtils } from '../useUtils'; import { FieldChangeHandlerContext } from '../useField'; -import { InferError, useValidation } from '../useValidation'; -import { FieldSection, PickerChangeHandlerContext, PickerValidDate } from '../../../models'; +import { useValidation } from '../../../validation'; +import { + FieldSection, + PickerChangeHandlerContext, + PickerValidDate, + InferError, +} from '../../../models'; import { PickerShortcutChangeImportance, PickersShortcutsItemContext, @@ -241,12 +246,13 @@ export const usePickerValue = < }; }); - useValidation( - { ...props, value: dateState.draft, timezone }, + const { getValidationErrorForNewValue } = useValidation({ + props, validator, - valueManager.isSameError, - valueManager.defaultErrorState, - ); + timezone, + value: dateState.draft, + onError: props.onError, + }); const updateDate = useEventCallback((action: PickerValueUpdateAction) => { const updaterParams: PickerValueUpdaterParams = { @@ -275,11 +281,7 @@ export const usePickerValue = < const validationError = action.name === 'setValueFromField' ? action.context.validationError - : validator({ - adapter, - value: action.value, - props: { ...props, value: action.value, timezone }, - }); + : getValidationErrorForNewValue(action.value); cachedContext = { validationError, @@ -440,7 +442,8 @@ export const usePickerValue = < const error = validator({ adapter, value: testedValue, - props: { ...props, value: testedValue, timezone }, + timezone, + props, }); return !valueManager.hasError(error); 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 d2a170a8728e..a2882f62f57f 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,6 +1,5 @@ import { FieldChangeHandlerContext, UseFieldInternalProps } from '../useField'; -import { InferError, Validator } from '../useValidation'; -import { UseFieldValidationProps } from '../useField/useField.types'; +import { Validator } from '../../../validation'; import { WrapperVariant } from '../../models/common'; import { FieldSection, @@ -10,6 +9,8 @@ import { PickersTimezone, PickerChangeHandlerContext, PickerValidDate, + OnErrorProps, + InferError, } from '../../../models'; import { GetDefaultReferenceDateProps } from '../../utils/getDefaultReferenceDate'; import { @@ -207,7 +208,7 @@ export type PickerValueUpdateAction = /** * Props used to handle the value that are common to all pickers. */ -export interface UsePickerValueBaseProps { +export interface UsePickerValueBaseProps extends OnErrorProps { /** * The selected value. * Used when the component is controlled. @@ -234,16 +235,6 @@ export interface UsePickerValueBaseProps { * @param {FieldChangeHandlerContext} context The context containing the validation result of the current value. */ onAccept?: (value: TValue, context: PickerChangeHandlerContext) => void; - /** - * Callback fired when the error associated with the current value changes. - * When a validation error is detected, the `error` parameter contains a non-null value. - * This can be used to render an appropriate form error. - * @template TError The validation error type. It will be either `string` or a `null`. It can be in `[start, end]` format in case of range value. - * @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value. - * @param {TError} error The reason why the current value is not valid. - * @param {TValue} value The value associated with the error. - */ - onError?: (error: TError, value: TValue) => void; } /** @@ -289,12 +280,7 @@ export interface UsePickerValueParams< valueManager: PickerValueManager>; valueType: FieldValueType; wrapperVariant: WrapperVariant; - validator: Validator< - TValue, - TDate, - InferError, - UseFieldValidationProps - >; + validator: Validator, TExternalProps>; } export interface UsePickerValueActions { diff --git a/packages/x-date-pickers/src/internals/hooks/useValidation.ts b/packages/x-date-pickers/src/internals/hooks/useValidation.ts deleted file mode 100644 index ce2fb99cfb29..000000000000 --- a/packages/x-date-pickers/src/internals/hooks/useValidation.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as React from 'react'; -import { useLocalizationContext } from './useUtils'; -import { MuiPickersAdapterContextValue } from '../../LocalizationProvider/LocalizationProvider'; -import { PickerValidDate } from '../../models'; - -interface ValidationCommonProps { - /** - * Callback fired when the error associated with the current value changes. - * When a validation error is detected, the `error` parameter contains a non-null value. - * This can be used to render an appropriate form error. - * @template TError The validation error type. It will be either `string` or a `null`. It can be in `[start, end]` format in case of range value. - * @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value. - * @param {TError} error The reason why the current value is not valid. - * @param {TValue} value The value associated with the error. - */ - onError?: (error: TError, value: TValue) => void; - value: TValue; -} - -export type ValidationProps = ValidationCommonProps< - TError, - TValue -> & - TValidationProps; - -export type InferError = - TProps extends Pick, 'onError'> - ? Parameters>[0] - : never; - -export type Validator = (params: { - adapter: MuiPickersAdapterContextValue; - value: TValue; - props: Omit; -}) => TError; - -export function useValidation< - TValue, - TDate extends PickerValidDate, - TError, - TValidationProps extends {}, ->( - props: ValidationProps, - validate: Validator, - isSameError: (a: TError, b: TError | null) => boolean, - defaultErrorState: TError, -): TError { - const { value, onError } = props; - const adapter = useLocalizationContext(); - const previousValidationErrorRef = React.useRef(defaultErrorState); - - const validationError = validate({ adapter, value, props }); - - React.useEffect(() => { - if (onError && !isSameError(validationError, previousValidationErrorRef.current)) { - onError(validationError, value); - } - - previousValidationErrorRef.current = validationError; - }, [isSameError, onError, previousValidationErrorRef, validationError, value]); - - return validationError; -} diff --git a/packages/x-date-pickers/src/internals/index.ts b/packages/x-date-pickers/src/internals/index.ts index 709f02096df7..6bd9dab4cd19 100644 --- a/packages/x-date-pickers/src/internals/index.ts +++ b/packages/x-date-pickers/src/internals/index.ts @@ -92,8 +92,6 @@ export type { export { useLocalizationContext, useDefaultDates, useUtils, useNow } from './hooks/useUtils'; export type { ExportedUseViewsOptions, UseViewsOptions } from './hooks/useViews'; export { useViews } from './hooks/useViews'; -export { useValidation } from './hooks/useValidation'; -export type { ValidationProps, Validator, InferError } from './hooks/useValidation'; export { usePreviousMonthDisabled, useNextMonthDisabled } from './hooks/date-helpers-hooks'; export type { BaseFieldProps } from './models/fields'; @@ -145,10 +143,6 @@ export { useDefaultizedDateTimeField, } from './hooks/defaultizedFieldProps'; export { useDefaultReduceAnimations } from './hooks/useDefaultReduceAnimations'; -export { extractValidationProps } from './utils/validation/extractValidationProps'; -export { validateDate } from './utils/validation/validateDate'; -export { validateDateTime } from './utils/validation/validateDateTime'; -export { validateTime } from './utils/validation/validateTime'; export { applyDefaultViewProps } from './utils/views'; export { DayCalendar } from '../DateCalendar/DayCalendar'; diff --git a/packages/x-date-pickers/src/internals/utils/validation/validateDateTime.ts b/packages/x-date-pickers/src/internals/utils/validation/validateDateTime.ts deleted file mode 100644 index ff14f93ac1eb..000000000000 --- a/packages/x-date-pickers/src/internals/utils/validation/validateDateTime.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Validator } from '../../hooks/useValidation'; -import { validateDate, DateComponentValidationProps } from './validateDate'; -import { validateTime, TimeComponentValidationProps } from './validateTime'; -import { DateTimeValidationError, PickerValidDate } from '../../../models'; - -export interface DateTimeComponentValidationProps - extends DateComponentValidationProps, - TimeComponentValidationProps {} - -export const validateDateTime: Validator< - any | null, - any, - DateTimeValidationError, - DateTimeComponentValidationProps -> = ({ props, value, adapter }) => { - const dateValidationResult = validateDate({ - adapter, - value, - props, - }); - - if (dateValidationResult !== null) { - return dateValidationResult; - } - - return validateTime({ - adapter, - value, - props, - }); -}; diff --git a/packages/x-date-pickers/src/models/validation.ts b/packages/x-date-pickers/src/models/validation.ts index 397ea4334d03..5dcee050db5f 100644 --- a/packages/x-date-pickers/src/models/validation.ts +++ b/packages/x-date-pickers/src/models/validation.ts @@ -21,3 +21,21 @@ export type TimeValidationError = | 'shouldDisableTime-seconds'; export type DateTimeValidationError = DateValidationError | TimeValidationError; + +export interface OnErrorProps { + /** + * Callback fired when the error associated with the current value changes. + * When a validation error is detected, the `error` parameter contains a non-null value. + * This can be used to render an appropriate form error. + * @template TError The validation error type. It will be either `string` or a `null`. It can be in `[start, end]` format in case of range value. + * @template TValue The value type. It will be the same type as `value` or `null`. It can be in `[start, end]` format in case of range value. + * @param {TError} error The reason why the current value is not valid. + * @param {TValue} value The value associated with the error. + */ + onError?: (error: TError, value: TValue) => void; +} + +export type InferError = + TProps extends Pick, 'onError'> + ? Parameters>[0] + : never; diff --git a/packages/x-date-pickers/src/internals/utils/validation/extractValidationProps.ts b/packages/x-date-pickers/src/validation/extractValidationProps.ts similarity index 97% rename from packages/x-date-pickers/src/internals/utils/validation/extractValidationProps.ts rename to packages/x-date-pickers/src/validation/extractValidationProps.ts index a2d90cde1510..3b2bd180137e 100644 --- a/packages/x-date-pickers/src/internals/utils/validation/extractValidationProps.ts +++ b/packages/x-date-pickers/src/validation/extractValidationProps.ts @@ -6,7 +6,7 @@ import { MonthValidationProps, TimeValidationProps, YearValidationProps, -} from '../../models/validation'; +} from '../internals/models/validation'; export const DATE_VALIDATION_PROP_NAMES: ( | keyof BaseDateValidationProps diff --git a/packages/x-date-pickers/src/validation/index.ts b/packages/x-date-pickers/src/validation/index.ts new file mode 100644 index 000000000000..b3f964c143f8 --- /dev/null +++ b/packages/x-date-pickers/src/validation/index.ts @@ -0,0 +1,13 @@ +export { validateDate } from './validateDate'; +export type { ValidateDateProps } from './validateDate'; + +export { validateTime } from './validateTime'; +export type { ValidateTimeProps } from './validateTime'; + +export { validateDateTime } from './validateDateTime'; +export type { ValidateDateTimeProps } from './validateDateTime'; + +export { extractValidationProps } from './extractValidationProps'; + +export { useValidation } from './useValidation'; +export type { Validator } from './useValidation'; diff --git a/packages/x-date-pickers/src/validation/useValidation.ts b/packages/x-date-pickers/src/validation/useValidation.ts new file mode 100644 index 000000000000..392d6780725a --- /dev/null +++ b/packages/x-date-pickers/src/validation/useValidation.ts @@ -0,0 +1,113 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { useLocalizationContext } from '../internals/hooks/useUtils'; +import { MuiPickersAdapterContextValue } from '../LocalizationProvider/LocalizationProvider'; +import { OnErrorProps, PickersTimezone, PickerValidDate } from '../models'; +import type { PickerValueManager } from '../internals/hooks/usePicker'; + +export type Validator = { + (params: { + adapter: MuiPickersAdapterContextValue; + value: TValue; + timezone: PickersTimezone; + props: TValidationProps; + }): TError; + valueManager: PickerValueManager; +}; + +interface UseValidationOptions< + TValue, + TDate extends PickerValidDate, + TError, + TValidationProps extends {}, +> extends OnErrorProps { + /** + * The value to validate. + */ + value: TValue; + /** + * The timezone to use for the validation. + */ + timezone: PickersTimezone; + /** + * The validator function to use. + * They can be imported from `@mui/x-date-pickers/validation` and `@mui/x-date-pickers-pro/validation`. + * It is recommended to only use the validator exported by the MUI X packages, + * otherwise you may have inconsistent behaviors between the field and the views. + */ + validator: Validator; + /** + * The validation props, they differ depending on the component. + * For example, the `validateTime` function supports `minTime`, `maxTime`, etc. + */ + props: TValidationProps; +} + +interface UseValidationReturnValue { + /** + * The validation error associated to the value passed to the `useValidation` hook. + */ + validationError: TError; + /** + * `true` if the current error is not null. + * For single value components, it means that the value is invalid. + * For range components, it means that either start or end value is invalid. + */ + hasValidationError: boolean; + /** + * Get the validation error for a new value. + * This can be used to validate the value in a change handler before updating the state. + * @template TValue The value type. + * @param {TValue} newValue The value to validate. + * @returns {TError} The validation error associated to the new value. + */ + getValidationErrorForNewValue: (newValue: TValue) => TError; +} + +/** + * Utility hook to check if a given value is valid based on the provided validation props. + * @template TDate + * @template TValue The value type. It will be either the same type as `value` or `null`. It can be in `[start, end]` format in case of range value. + * @template TError The validation error type. It will be either `string` or a `null`. It can be in `[start, end]` format in case of range value. + * @param {UseValidationOptions} options The options to configure the hook. + * @param {TValue} options.value The value to validate. + * @param {PickersTimezone} options.timezone The timezone to use for the validation. + * @param {Validator} options.validator The validator function to use. + * @param {TValidationProps} options.props The validation props, they differ depending on the component. + * @param {(error: TError, value: TValue) => void} options.onError Callback fired when the error associated with the current value changes. + */ +export function useValidation< + TValue, + TDate extends PickerValidDate, + TError, + TValidationProps extends {}, +>( + options: UseValidationOptions, +): UseValidationReturnValue { + const { props, validator, value, timezone, onError } = options; + + const adapter = useLocalizationContext(); + const previousValidationErrorRef = React.useRef( + validator.valueManager.defaultErrorState, + ); + + const validationError = validator({ adapter, value, timezone, props }); + const hasValidationError = validator.valueManager.hasError(validationError); + + React.useEffect(() => { + if ( + onError && + !validator.valueManager.isSameError(validationError, previousValidationErrorRef.current) + ) { + onError(validationError, value); + } + + previousValidationErrorRef.current = validationError; + }, [validator, onError, validationError, value]); + + const getValidationErrorForNewValue = useEventCallback((newValue: TValue) => { + return validator({ adapter, value: newValue, timezone, props }); + }); + + return { validationError, hasValidationError, getValidationErrorForNewValue }; +} diff --git a/packages/x-date-pickers/src/internals/utils/validation/validateDate.ts b/packages/x-date-pickers/src/validation/validateDate.ts similarity index 67% rename from packages/x-date-pickers/src/internals/utils/validation/validateDate.ts rename to packages/x-date-pickers/src/validation/validateDate.ts index 363b13a96f70..e3bf707969aa 100644 --- a/packages/x-date-pickers/src/internals/utils/validation/validateDate.ts +++ b/packages/x-date-pickers/src/validation/validateDate.ts @@ -1,39 +1,32 @@ -import { Validator } from '../../hooks/useValidation'; +import { Validator } from './useValidation'; import { BaseDateValidationProps, DayValidationProps, MonthValidationProps, YearValidationProps, -} from '../../models/validation'; -import { DateValidationError, PickerValidDate, TimezoneProps } from '../../../models'; -import { applyDefaultDate } from '../date-utils'; -import { DefaultizedProps } from '../../models/helpers'; +} from '../internals/models/validation'; +import { DateValidationError, PickerValidDate } from '../models'; +import { applyDefaultDate } from '../internals/utils/date-utils'; +import { singleItemValueManager } from '../internals/utils/valueManagers'; -export interface DateComponentValidationProps +export interface ValidateDateProps extends DayValidationProps, MonthValidationProps, YearValidationProps, - Required>, - DefaultizedProps {} + Required> {} export const validateDate: Validator< any | null, any, DateValidationError, - DateComponentValidationProps -> = ({ props, value, adapter }): DateValidationError => { + ValidateDateProps +> = ({ props, value, timezone, adapter }): DateValidationError => { if (value === null) { return null; } - const { - shouldDisableDate, - shouldDisableMonth, - shouldDisableYear, - disablePast, - disableFuture, - timezone, - } = props; + const { shouldDisableDate, shouldDisableMonth, shouldDisableYear, disablePast, disableFuture } = + props; const now = adapter.utils.date(undefined, timezone); const minDate = applyDefaultDate(adapter.utils, props.minDate, adapter.defaultDates.minDate); @@ -68,3 +61,5 @@ export const validateDate: Validator< return null; } }; + +validateDate.valueManager = singleItemValueManager; diff --git a/packages/x-date-pickers/src/validation/validateDateTime.ts b/packages/x-date-pickers/src/validation/validateDateTime.ts new file mode 100644 index 000000000000..1e4824635532 --- /dev/null +++ b/packages/x-date-pickers/src/validation/validateDateTime.ts @@ -0,0 +1,36 @@ +import { Validator } from './useValidation'; +import { validateDate, ValidateDateProps } from './validateDate'; +import { validateTime, ValidateTimeProps } from './validateTime'; +import { DateTimeValidationError, PickerValidDate } from '../models'; +import { singleItemValueManager } from '../internals/utils/valueManagers'; + +export interface ValidateDateTimeProps + extends ValidateDateProps, + ValidateTimeProps {} + +export const validateDateTime: Validator< + any | null, + any, + DateTimeValidationError, + ValidateDateTimeProps +> = ({ adapter, value, timezone, props }) => { + const dateValidationResult = validateDate({ + adapter, + value, + timezone, + props, + }); + + if (dateValidationResult !== null) { + return dateValidationResult; + } + + return validateTime({ + adapter, + value, + timezone, + props, + }); +}; + +validateDateTime.valueManager = singleItemValueManager; diff --git a/packages/x-date-pickers/src/internals/utils/validation/validateTime.ts b/packages/x-date-pickers/src/validation/validateTime.ts similarity index 73% rename from packages/x-date-pickers/src/internals/utils/validation/validateTime.ts rename to packages/x-date-pickers/src/validation/validateTime.ts index c3d109e7ec0f..2124913a3c14 100644 --- a/packages/x-date-pickers/src/internals/utils/validation/validateTime.ts +++ b/packages/x-date-pickers/src/validation/validateTime.ts @@ -1,20 +1,19 @@ -import { createIsAfterIgnoreDatePart } from '../time-utils'; -import { Validator } from '../../hooks/useValidation'; -import { BaseTimeValidationProps, TimeValidationProps } from '../../models/validation'; -import { PickerValidDate, TimeValidationError, TimezoneProps } from '../../../models'; -import { DefaultizedProps } from '../../models/helpers'; +import { createIsAfterIgnoreDatePart } from '../internals/utils/time-utils'; +import { Validator } from './useValidation'; +import { BaseTimeValidationProps, TimeValidationProps } from '../internals/models/validation'; +import { PickerValidDate, TimeValidationError } from '../models'; +import { singleItemValueManager } from '../internals/utils/valueManagers'; -export interface TimeComponentValidationProps +export interface ValidateTimeProps extends Required, - TimeValidationProps, - DefaultizedProps {} + TimeValidationProps {} export const validateTime: Validator< any | null, any, TimeValidationError, - TimeComponentValidationProps -> = ({ adapter, value, props }): TimeValidationError => { + ValidateTimeProps +> = ({ adapter, value, timezone, props }): TimeValidationError => { if (value === null) { return null; } @@ -27,7 +26,6 @@ export const validateTime: Validator< disableIgnoringDatePartForTimeValidation = false, disablePast, disableFuture, - timezone, } = props; const now = adapter.utils.date(undefined, timezone); @@ -68,3 +66,5 @@ export const validateTime: Validator< return null; } }; + +validateTime.valueManager = singleItemValueManager; diff --git a/scripts/x-date-pickers-pro.exports.json b/scripts/x-date-pickers-pro.exports.json index 9674b138dfd2..24beed350813 100644 --- a/scripts/x-date-pickers-pro.exports.json +++ b/scripts/x-date-pickers-pro.exports.json @@ -34,6 +34,7 @@ { "name": "DateCalendarSlotProps", "kind": "Interface" }, { "name": "DateCalendarSlots", "kind": "Interface" }, { "name": "DateField", "kind": "Variable" }, + { "name": "DateFieldInPickerProps", "kind": "TypeAlias" }, { "name": "DateFieldProps", "kind": "TypeAlias" }, { "name": "DateOrTimeView", "kind": "TypeAlias" }, { "name": "DatePicker", "kind": "Variable" }, @@ -71,6 +72,7 @@ { "name": "DateRangeValidationError", "kind": "TypeAlias" }, { "name": "DateRangeViewRendererProps", "kind": "Interface" }, { "name": "DateTimeField", "kind": "Variable" }, + { "name": "DateTimeFieldInPickerProps", "kind": "TypeAlias" }, { "name": "DateTimeFieldProps", "kind": "TypeAlias" }, { "name": "DateTimePicker", "kind": "Variable" }, { "name": "DateTimePickerProps", "kind": "Interface" }, @@ -154,6 +156,7 @@ { "name": "ExportedPickersYearProps", "kind": "Interface" }, { "name": "ExportedSlideTransitionProps", "kind": "Interface" }, { "name": "ExportedUseClearableFieldProps", "kind": "Interface" }, + { "name": "extractValidationProps", "kind": "Variable" }, { "name": "FieldFormatTokenMap", "kind": "TypeAlias" }, { "name": "FieldRef", "kind": "Interface" }, { "name": "FieldSection", "kind": "Interface" }, @@ -184,6 +187,7 @@ { "name": "getPickersTextFieldUtilityClass", "kind": "Function" }, { "name": "getTimeClockUtilityClass", "kind": "Function" }, { "name": "getYearCalendarUtilityClass", "kind": "Function" }, + { "name": "InferError", "kind": "TypeAlias" }, { "name": "LicenseInfo", "kind": "Class" }, { "name": "LocalizationProvider", "kind": "Variable" }, { "name": "LocalizationProviderProps", "kind": "Interface" }, @@ -242,6 +246,7 @@ { "name": "MultiSectionDigitalClockSlotProps", "kind": "Interface" }, { "name": "MultiSectionDigitalClockSlots", "kind": "Interface" }, { "name": "NonEmptyDateRange", "kind": "TypeAlias" }, + { "name": "OnErrorProps", "kind": "Interface" }, { "name": "PickerChangeHandlerContext", "kind": "Interface" }, { "name": "PickersActionBar", "kind": "Function" }, { "name": "PickersActionBarAction", "kind": "TypeAlias" }, @@ -367,6 +372,7 @@ { "name": "TimeClockSlotProps", "kind": "Interface" }, { "name": "TimeClockSlots", "kind": "Interface" }, { "name": "TimeField", "kind": "Variable" }, + { "name": "TimeFieldInPickerProps", "kind": "TypeAlias" }, { "name": "TimeFieldProps", "kind": "TypeAlias" }, { "name": "TimeIcon", "kind": "Variable" }, { "name": "TimePicker", "kind": "Variable" }, @@ -421,6 +427,20 @@ { "name": "useSplitFieldProps", "kind": "Variable" }, { "name": "UseTimeFieldComponentProps", "kind": "TypeAlias" }, { "name": "UseTimeFieldProps", "kind": "Interface" }, + { "name": "useValidation", "kind": "Function" }, + { "name": "validateDate", "kind": "Variable" }, + { "name": "ValidateDateProps", "kind": "Interface" }, + { "name": "validateDateRange", "kind": "Variable" }, + { "name": "ValidateDateRangeProps", "kind": "Interface" }, + { "name": "validateDateTime", "kind": "Variable" }, + { "name": "ValidateDateTimeProps", "kind": "Interface" }, + { "name": "validateDateTimeRange", "kind": "Variable" }, + { "name": "ValidateDateTimeRangeProps", "kind": "Interface" }, + { "name": "validateTime", "kind": "Variable" }, + { "name": "ValidateTimeProps", "kind": "Interface" }, + { "name": "validateTimeRange", "kind": "Variable" }, + { "name": "ValidateTimeRangeProps", "kind": "Interface" }, + { "name": "Validator", "kind": "TypeAlias" }, { "name": "YearCalendar", "kind": "Variable" }, { "name": "yearCalendarClasses", "kind": "Variable" }, { "name": "YearCalendarClasses", "kind": "Interface" }, diff --git a/scripts/x-date-pickers.exports.json b/scripts/x-date-pickers.exports.json index 1df3b6ad87cb..4ae16b168cba 100644 --- a/scripts/x-date-pickers.exports.json +++ b/scripts/x-date-pickers.exports.json @@ -31,6 +31,7 @@ { "name": "DateCalendarSlotProps", "kind": "Interface" }, { "name": "DateCalendarSlots", "kind": "Interface" }, { "name": "DateField", "kind": "Variable" }, + { "name": "DateFieldInPickerProps", "kind": "TypeAlias" }, { "name": "DateFieldProps", "kind": "TypeAlias" }, { "name": "DateOrTimeView", "kind": "TypeAlias" }, { "name": "DatePicker", "kind": "Variable" }, @@ -44,6 +45,7 @@ { "name": "DatePickerToolbarProps", "kind": "Interface" }, { "name": "DateRangeIcon", "kind": "Variable" }, { "name": "DateTimeField", "kind": "Variable" }, + { "name": "DateTimeFieldInPickerProps", "kind": "TypeAlias" }, { "name": "DateTimeFieldProps", "kind": "TypeAlias" }, { "name": "DateTimePicker", "kind": "Variable" }, { "name": "DateTimePickerProps", "kind": "Interface" }, @@ -102,6 +104,7 @@ { "name": "ExportedPickersYearProps", "kind": "Interface" }, { "name": "ExportedSlideTransitionProps", "kind": "Interface" }, { "name": "ExportedUseClearableFieldProps", "kind": "Interface" }, + { "name": "extractValidationProps", "kind": "Variable" }, { "name": "FieldFormatTokenMap", "kind": "TypeAlias" }, { "name": "FieldRef", "kind": "Interface" }, { "name": "FieldSection", "kind": "Interface" }, @@ -123,6 +126,7 @@ { "name": "getPickersTextFieldUtilityClass", "kind": "Function" }, { "name": "getTimeClockUtilityClass", "kind": "Function" }, { "name": "getYearCalendarUtilityClass", "kind": "Function" }, + { "name": "InferError", "kind": "TypeAlias" }, { "name": "LocalizationProvider", "kind": "Variable" }, { "name": "LocalizationProviderProps", "kind": "Interface" }, { "name": "LocalizedComponent", "kind": "TypeAlias" }, @@ -157,6 +161,7 @@ { "name": "MultiSectionDigitalClockSectionClassKey", "kind": "TypeAlias" }, { "name": "MultiSectionDigitalClockSlotProps", "kind": "Interface" }, { "name": "MultiSectionDigitalClockSlots", "kind": "Interface" }, + { "name": "OnErrorProps", "kind": "Interface" }, { "name": "PickerChangeHandlerContext", "kind": "Interface" }, { "name": "PickersActionBar", "kind": "Function" }, { "name": "PickersActionBarAction", "kind": "TypeAlias" }, @@ -266,6 +271,7 @@ { "name": "TimeClockSlotProps", "kind": "Interface" }, { "name": "TimeClockSlots", "kind": "Interface" }, { "name": "TimeField", "kind": "Variable" }, + { "name": "TimeFieldInPickerProps", "kind": "TypeAlias" }, { "name": "TimeFieldProps", "kind": "TypeAlias" }, { "name": "TimeIcon", "kind": "Variable" }, { "name": "TimePicker", "kind": "Variable" }, @@ -303,6 +309,14 @@ { "name": "useSplitFieldProps", "kind": "Variable" }, { "name": "UseTimeFieldComponentProps", "kind": "TypeAlias" }, { "name": "UseTimeFieldProps", "kind": "Interface" }, + { "name": "useValidation", "kind": "Function" }, + { "name": "validateDate", "kind": "Variable" }, + { "name": "ValidateDateProps", "kind": "Interface" }, + { "name": "validateDateTime", "kind": "Variable" }, + { "name": "ValidateDateTimeProps", "kind": "Interface" }, + { "name": "validateTime", "kind": "Variable" }, + { "name": "ValidateTimeProps", "kind": "Interface" }, + { "name": "Validator", "kind": "TypeAlias" }, { "name": "YearCalendar", "kind": "Variable" }, { "name": "yearCalendarClasses", "kind": "Variable" }, { "name": "YearCalendarClasses", "kind": "Interface" },