diff --git a/docs/data/date-pickers/date-range-field/BasicDateRangeField.js b/docs/data/date-pickers/date-range-field/BasicDateRangeField.js index 06e7c389b07f8..6cc983bc9b2ee 100644 --- a/docs/data/date-pickers/date-range-field/BasicDateRangeField.js +++ b/docs/data/date-pickers/date-range-field/BasicDateRangeField.js @@ -6,11 +6,18 @@ import { Unstable_MultiInputDateRangeField as MultiInputDateRangeField } from '@ import { Unstable_SingleInputDateRangeField as SingleInputDateRangeField } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; export default function BasicDateRangeField() { + const [value, setValue] = React.useState([null, null]); return ( - - + setValue(newValue)} + /> + setValue(newValue)} + /> ); diff --git a/docs/data/date-pickers/date-range-field/BasicDateRangeField.tsx b/docs/data/date-pickers/date-range-field/BasicDateRangeField.tsx index 06e7c389b07f8..92cdc1f0f77b9 100644 --- a/docs/data/date-pickers/date-range-field/BasicDateRangeField.tsx +++ b/docs/data/date-pickers/date-range-field/BasicDateRangeField.tsx @@ -6,11 +6,18 @@ import { Unstable_MultiInputDateRangeField as MultiInputDateRangeField } from '@ import { Unstable_SingleInputDateRangeField as SingleInputDateRangeField } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; export default function BasicDateRangeField() { + const [value, setValue] = React.useState([null, null]); return ( - - + setValue(newValue)} + /> + setValue(newValue)} + /> ); diff --git a/docs/data/date-pickers/date-range-field/BasicDateRangeField.tsx.preview b/docs/data/date-pickers/date-range-field/BasicDateRangeField.tsx.preview index 13214cf368c44..ed200657dde99 100644 --- a/docs/data/date-pickers/date-range-field/BasicDateRangeField.tsx.preview +++ b/docs/data/date-pickers/date-range-field/BasicDateRangeField.tsx.preview @@ -1,6 +1,12 @@ - - + setValue(newValue)} + /> + setValue(newValue)} + /> \ No newline at end of file diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerInput.tsx b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerInput.tsx index 0e7cdbfbe753a..a892527288f07 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerInput.tsx +++ b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerInput.tsx @@ -11,7 +11,7 @@ import { onSpaceOrEnter, useLocaleText, } from '@mui/x-date-pickers/internals'; -import { CurrentlySelectingRangeEndProps, DateRange } from '../internal/models/dateRange'; +import { CurrentlySelectingRangeEndProps, DateRange } from '../internal/models/range'; import { DateRangeValidationError } from '../internal/hooks/validation/useDateRangeValidation'; import { DateRangePickerInputClasses, diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerView.tsx b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerView.tsx index c6219a6f2d5d4..e80d96da0865a 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerView.tsx +++ b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerView.tsx @@ -13,11 +13,8 @@ import { BaseDateValidationProps, DayValidationProps, } from '@mui/x-date-pickers/internals'; -import { - DateRange, - CurrentlySelectingRangeEndProps, - DayRangeValidationProps, -} from '../internal/models/dateRange'; +import { DateRange, CurrentlySelectingRangeEndProps } from '../internal/models/range'; +import { DayRangeValidationProps } from '../internal/models/dateRange'; import { isRangeValid } from '../internal/utils/date-utils'; import { calculateRangeChange } from './date-range-manager'; import { DateRangePickerToolbar } from './DateRangePickerToolbar'; diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerViewDesktop.tsx b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerViewDesktop.tsx index 7087cb4b21dd8..1b9cfec7c9ef6 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerViewDesktop.tsx +++ b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerViewDesktop.tsx @@ -21,7 +21,7 @@ import { DayCalendarSlotsComponentsProps, } from '@mui/x-date-pickers/internals'; import { calculateRangePreview } from './date-range-manager'; -import { DateRange } from '../internal/models'; +import { DateRange } from '../internal/models/range'; import { DateRangePickerDay, DateRangePickerDayProps } from '../DateRangePickerDay'; import { isWithinRange, isStartOfRange, isEndOfRange } from '../internal/utils/date-utils'; import { doNothing } from '../internal/utils/utils'; diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerViewMobile.tsx b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerViewMobile.tsx index c785d31c9fa20..7f20f0fa8416d 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerViewMobile.tsx +++ b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePickerViewMobile.tsx @@ -14,8 +14,9 @@ import { DayCalendarSlotsComponentsProps, } from '@mui/x-date-pickers/internals'; import { doNothing } from '../internal/utils/utils'; -import { DateRange } from '../internal/models/dateRange'; +import { DateRange } from '../internal/models/range'; import { DateRangePickerDay, DateRangePickerDayProps } from '../DateRangePickerDay'; + import { isWithinRange, isStartOfRange, isEndOfRange } from '../internal/utils/date-utils'; export interface DateRangePickerViewMobileSlotsComponent diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/date-range-manager.test.ts b/packages/x-date-pickers-pro/src/DateRangePicker/date-range-manager.test.ts index 6847065b5a34d..f2a12ae188c90 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/date-range-manager.test.ts +++ b/packages/x-date-pickers-pro/src/DateRangePicker/date-range-manager.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { adapterToUse } from 'test/utils/pickers-utils'; import { calculateRangeChange, calculateRangePreview } from './date-range-manager'; -import { DateRange } from '../internal/models/dateRange'; +import { DateRange } from '../internal/models/range'; const start2018 = adapterToUse.date(new Date(2018, 0, 1)); const mid2018 = adapterToUse.date(new Date(2018, 6, 1)); diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/shared.ts b/packages/x-date-pickers-pro/src/DateRangePicker/shared.ts index ece2743486c1e..1d51914778197 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/shared.ts +++ b/packages/x-date-pickers-pro/src/DateRangePicker/shared.ts @@ -67,6 +67,7 @@ export function useDateRangePickerDefaultizedProps< }; } +// What about renaming it `rangePickerValueManager` such that it's clear this manager is common to date, time and dateTime? export const dateRangePickerValueManager: PickerStateValueManager<[any, any], any> = { emptyValue: [null, null], getTodayValue: (utils) => [utils.date()!, utils.date()!], diff --git a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.interfaces.ts b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.interfaces.ts deleted file mode 100644 index 02793d82c4d19..0000000000000 --- a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.interfaces.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { UseSingleInputDateRangeFieldProps } from '../SingleInputDateRangeField'; - -export interface UseMultiInputDateRangeFieldProps - extends UseSingleInputDateRangeFieldProps {} - -export interface MultiInputDateRangeFieldProps - extends UseMultiInputDateRangeFieldProps {} diff --git a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx index e7f91fa5b4448..90aa0617bf30b 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx @@ -5,7 +5,7 @@ import Typography, { TypographyProps } from '@mui/material/Typography'; import { styled, useThemeProps } from '@mui/material/styles'; import { useSlotProps } from '@mui/base/utils'; import { MultiInputDateRangeFieldProps } from './MultiInputDateRangeField.types'; -import { useMultiInputDateRangeField } from './useMultiInputDateRangeField'; +import { useMultiInputDateRangeField } from '../internal/hooks/useMultiInputRangeField/useMultiInputDateRangeField'; const MultiInputDateRangeFieldRoot = styled( React.forwardRef((props: StackProps, ref: React.Ref) => ( diff --git a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.types.ts b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.types.ts index f10af189973a1..f4ebdb976c351 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.types.ts @@ -3,18 +3,18 @@ import { SlotComponentProps } from '@mui/base/utils'; import Typography from '@mui/material/Typography'; import Stack from '@mui/material/Stack'; import TextField from '@mui/material/TextField'; -import { UseSingleInputDateRangeFieldProps } from '../SingleInputDateRangeField'; +import { UseDateRangeFieldProps } from '../internal/models/dateRange'; export interface UseMultiInputDateRangeFieldParams { - sharedProps: UseMultiInputDateRangeFieldComponentProps; + sharedProps: Omit> & + UseMultiInputDateRangeFieldProps; startInputProps: TChildProps; endInputProps: TChildProps; startInputRef?: React.Ref; endInputRef?: React.Ref; } -export interface UseMultiInputDateRangeFieldProps - extends UseSingleInputDateRangeFieldProps {} +export interface UseMultiInputDateRangeFieldProps extends UseDateRangeFieldProps {} export type UseMultiInputDateRangeFieldComponentProps = Omit< TChildProps, diff --git a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx new file mode 100644 index 0000000000000..c6031f018f45c --- /dev/null +++ b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx @@ -0,0 +1,147 @@ +import * as React from 'react'; +import Stack, { StackProps } from '@mui/material/Stack'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; +import Typography, { TypographyProps } from '@mui/material/Typography'; +import { styled, useThemeProps } from '@mui/material/styles'; +import { useSlotProps } from '@mui/base/utils'; +import { MultiInputDateTimeRangeFieldProps } from './MultiInputDateTimeRangeField.types'; +import { useMultiInputDateTimeRangeField } from '../internal/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField'; + +const MultiInputDateTimeRangeFieldRoot = styled( + React.forwardRef((props: StackProps, ref: React.Ref) => ( + + )), + { + name: 'MuiMultiInputDateTimeRangeField', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, + }, +)({}); + +const MultiInputDateTimeRangeFieldSeparator = styled( + (props: TypographyProps) => {props.children ?? ' — '}, + { + name: 'MuiMultiInputDateTimeRangeField', + slot: 'Separator', + overridesResolver: (props, styles) => styles.separator, + }, +)({}); + +type MultiInputDateTimeRangeFieldComponent = (( + props: MultiInputDateTimeRangeFieldProps & React.RefAttributes, +) => JSX.Element) & { propTypes?: any }; + +export const MultiInputDateTimeRangeField = React.forwardRef(function MultiInputDateTimeRangeField< + TDate, +>(inProps: MultiInputDateTimeRangeFieldProps, ref: React.Ref) { + const themeProps = useThemeProps({ + props: inProps, + name: 'MuiMultiInputDateTimeRangeField', + }); + + const { + components, + componentsProps, + value, + defaultValue, + format, + onChange, + readOnly, + onError, + shouldDisableDate, + minDate, + maxDate, + minTime, + maxTime, + minDateTime, + maxDateTime, + minutesStep, + shouldDisableTime, + disableFuture, + disablePast, + ...other + } = themeProps; + + const ownerState = themeProps; + + const Root = components?.Root ?? MultiInputDateTimeRangeFieldRoot; + const rootProps = useSlotProps({ + elementType: Root, + externalSlotProps: componentsProps?.root, + externalForwardedProps: other, + additionalProps: { + ref, + }, + ownerState, + }); + + const Input = components?.Input ?? TextField; + const startInputProps: TextFieldProps = useSlotProps({ + elementType: Input, + externalSlotProps: componentsProps?.input, + ownerState: { ...ownerState, position: 'start' }, + }); + const endInputProps: TextFieldProps = useSlotProps({ + elementType: Input, + externalSlotProps: componentsProps?.input, + ownerState: { ...ownerState, position: 'end' }, + }); + + const Separator = components?.Separator ?? MultiInputDateTimeRangeFieldSeparator; + const separatorProps = useSlotProps({ + elementType: Separator, + externalSlotProps: componentsProps?.separator, + ownerState, + }); + + const { + startDate: { onKeyDown: onStartInputKeyDown, ref: startInputRef, ...startDateProps }, + endDate: { onKeyDown: onEndInputKeyDown, ref: endInputRef, ...endDateProps }, + } = useMultiInputDateTimeRangeField({ + sharedProps: { + value, + defaultValue, + format, + onChange, + readOnly, + onError, + shouldDisableDate, + minDate, + maxDate, + minTime, + maxTime, + minDateTime, + maxDateTime, + minutesStep, + shouldDisableTime, + disableFuture, + disablePast, + }, + startInputProps, + endInputProps, + startInputRef: startInputProps.inputRef, + endInputRef: endInputProps.inputRef, + }); + + return ( + + + + + + ); +}) as MultiInputDateTimeRangeFieldComponent; diff --git a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types.ts b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types.ts new file mode 100644 index 0000000000000..2750e4b4757fd --- /dev/null +++ b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types.ts @@ -0,0 +1,79 @@ +import * as React from 'react'; +import { SlotComponentProps } from '@mui/base/utils'; +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; +import { + UseDateTimeRangeFieldDefaultizedProps, + UseDateTimeRangeFieldProps, +} from '../internal/models/dateTimeRange'; + +export interface UseMultiInputDateTimeRangeFieldParams { + sharedProps: Omit> & + UseMultiInputDateTimeRangeFieldProps; + startInputProps: TChildProps; + endInputProps: TChildProps; + startInputRef?: React.Ref; + endInputRef?: React.Ref; +} + +export interface UseMultiInputDateTimeRangeFieldProps + extends UseDateTimeRangeFieldProps {} + +export type UseMultiInputDateTimeRangeFieldComponentProps = Omit< + TChildProps, + keyof UseMultiInputDateTimeRangeFieldProps +> & + UseMultiInputDateTimeRangeFieldProps; + +export interface MultiInputDateTimeRangeFieldProps + extends UseMultiInputDateTimeRangeFieldComponentProps { + /** + * Overrideable components. + * @default {} + */ + components?: MultiInputDateTimeRangeFieldSlotsComponent; + /** + * The props used for each component slot. + * @default {} + */ + componentsProps?: MultiInputDateTimeRangeFieldSlotsComponentsProps; +} + +export type MultiInputDateTimeRangeFieldOwnerState = + MultiInputDateTimeRangeFieldProps; + +export interface MultiInputDateTimeRangeFieldSlotsComponent { + /** + * Element rendered at the root. + * @default MultiInputDateTimeRangeFieldRoot + */ + Root?: React.ElementType; + /** + * Input rendered for the start or end date. + * @default TextField + */ + Input?: React.ElementType; + /** + * Element rendered between the two inputs. + * @default MultiInputDateTimeRangeFieldSeparator + */ + Separator?: React.ElementType; +} + +export interface MultiInputDateTimeRangeFieldSlotsComponentsProps { + root?: SlotComponentProps>; + input?: SlotComponentProps< + typeof TextField, + {}, + MultiInputDateTimeRangeFieldOwnerState & { position: 'start' | 'end' } + >; + separator?: SlotComponentProps< + typeof Typography, + {}, + MultiInputDateTimeRangeFieldOwnerState + >; +} + +export type UseMultiInputDateTimeRangeFieldDefaultizedProps = + UseDateTimeRangeFieldDefaultizedProps; diff --git a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/index.ts b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/index.ts new file mode 100644 index 0000000000000..741f9d78a60b0 --- /dev/null +++ b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/index.ts @@ -0,0 +1,2 @@ +export { MultiInputDateTimeRangeField as Unstable_MultiInputDateTimeRangeField } from './MultiInputDateTimeRangeField'; +export type { UseMultiInputDateTimeRangeFieldProps } from './MultiInputDateTimeRangeField.types'; diff --git a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/tests/MultiInputDateTimeRangeField.conformance.test.tsx b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/tests/MultiInputDateTimeRangeField.conformance.test.tsx new file mode 100644 index 0000000000000..8a0a21d2ae7c3 --- /dev/null +++ b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/tests/MultiInputDateTimeRangeField.conformance.test.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { describeConformance } from '@mui/monorepo/test/utils'; +import { Unstable_MultiInputDateTimeRangeField as MultiInputDateTimeRangeField } from '@mui/x-date-pickers-pro/MultiInputDateTimeRangeField'; +import { createPickerRenderer, wrapPickerMount } from 'test/utils/pickers-utils'; + +describe('', () => { + const { render } = createPickerRenderer(); + + describeConformance(, () => ({ + classes: {}, + inheritComponent: 'div', + render, + muiName: 'MuiMultiInputDateTimeRangeField', + wrapMount: wrapPickerMount, + refInstanceof: window.HTMLDivElement, + // cannot test reactTestRenderer because of required context + skip: ['reactTestRenderer', 'themeVariants'], + })); +}); diff --git a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx new file mode 100644 index 0000000000000..51507041bd83d --- /dev/null +++ b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx @@ -0,0 +1,138 @@ +import * as React from 'react'; +import Stack, { StackProps } from '@mui/material/Stack'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; +import Typography, { TypographyProps } from '@mui/material/Typography'; +import { styled, useThemeProps } from '@mui/material/styles'; +import { useSlotProps } from '@mui/base/utils'; +import { MultiInputTimeRangeFieldProps } from './MultiInputTimeRangeField.types'; +import { useMultiInputTimeRangeField } from '../internal/hooks/useMultiInputRangeField/useMultiInputTimeRangeField'; + +const MultiInputTimeRangeFieldRoot = styled( + React.forwardRef((props: StackProps, ref: React.Ref) => ( + + )), + { + name: 'MuiMultiInputTimeRangeField', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, + }, +)({}); + +const MultiInputTimeRangeFieldSeparator = styled( + (props: TypographyProps) => {props.children ?? ' — '}, + { + name: 'MuiMultiInputTimeRangeField', + slot: 'Separator', + overridesResolver: (props, styles) => styles.separator, + }, +)({}); + +type MultiInputTimeRangeFieldComponent = (( + props: MultiInputTimeRangeFieldProps & React.RefAttributes, +) => JSX.Element) & { propTypes?: any }; + +export const MultiInputTimeRangeField = React.forwardRef(function MultiInputTimeRangeField( + inProps: MultiInputTimeRangeFieldProps, + ref: React.Ref, +) { + const themeProps = useThemeProps({ + props: inProps, + name: 'MuiMultiInputTimeRangeField', + }); + + const { + components, + componentsProps, + value, + defaultValue, + format, + onChange, + readOnly, + onError, + minTime, + maxTime, + minutesStep, + shouldDisableTime, + disableFuture, + disablePast, + ...other + } = themeProps; + + const ownerState = themeProps; + + const Root = components?.Root ?? MultiInputTimeRangeFieldRoot; + const rootProps = useSlotProps({ + elementType: Root, + externalSlotProps: componentsProps?.root, + externalForwardedProps: other, + additionalProps: { + ref, + }, + ownerState, + }); + + const Input = components?.Input ?? TextField; + const startInputProps: TextFieldProps = useSlotProps({ + elementType: Input, + externalSlotProps: componentsProps?.input, + ownerState: { ...ownerState, position: 'start' }, + }); + const endInputProps: TextFieldProps = useSlotProps({ + elementType: Input, + externalSlotProps: componentsProps?.input, + ownerState: { ...ownerState, position: 'end' }, + }); + + const Separator = components?.Separator ?? MultiInputTimeRangeFieldSeparator; + const separatorProps = useSlotProps({ + elementType: Separator, + externalSlotProps: componentsProps?.separator, + ownerState, + }); + + const { + startDate: { onKeyDown: onStartInputKeyDown, ref: startInputRef, ...startDateProps }, + endDate: { onKeyDown: onEndInputKeyDown, ref: endInputRef, ...endDateProps }, + } = useMultiInputTimeRangeField({ + sharedProps: { + value, + defaultValue, + format, + onChange, + readOnly, + onError, + minTime, + maxTime, + minutesStep, + shouldDisableTime, + disableFuture, + disablePast, + }, + startInputProps, + endInputProps, + startInputRef: startInputProps.inputRef, + endInputRef: endInputProps.inputRef, + }); + + return ( + + + + + + ); +}) as MultiInputTimeRangeFieldComponent; diff --git a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.types.ts b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.types.ts new file mode 100644 index 0000000000000..936705d721956 --- /dev/null +++ b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.types.ts @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { SlotComponentProps } from '@mui/base/utils'; +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import { + UseTimeRangeFieldDefaultizedProps, + UseTimeRangeFieldProps, +} from '../internal/models/timeRange'; + +export interface UseMultiInputTimeRangeFieldParams { + sharedProps: Omit> & + UseMultiInputTimeRangeFieldProps; + startInputProps: TChildProps; + endInputProps: TChildProps; + startInputRef?: React.Ref; + endInputRef?: React.Ref; +} + +export interface UseMultiInputTimeRangeFieldProps extends UseTimeRangeFieldProps {} + +export type UseMultiInputTimeRangeFieldComponentProps = Omit< + TChildProps, + keyof UseMultiInputTimeRangeFieldProps +> & + UseMultiInputTimeRangeFieldProps; + +export interface MultiInputTimeRangeFieldProps + extends UseMultiInputTimeRangeFieldComponentProps { + /** + * Overrideable components. + * @default {} + */ + components?: MultiInputTimeRangeFieldSlotsComponent; + /** + * The props used for each component slot. + * @default {} + */ + componentsProps?: MultiInputTimeRangeFieldSlotsComponentsProps; +} + +export type MultiInputTimeRangeFieldOwnerState = MultiInputTimeRangeFieldProps; + +export interface MultiInputTimeRangeFieldSlotsComponent { + /** + * Element rendered at the root. + * @default MultiInputTimeRangeFieldRoot + */ + Root?: React.ElementType; + /** + * Input rendered for the start or end date. + * @default TextField + */ + Input?: React.ElementType; + /** + * Element rendered between the two inputs. + * @default MultiInputTimeRangeFieldSeparator + */ + Separator?: React.ElementType; +} + +export interface MultiInputTimeRangeFieldSlotsComponentsProps { + root?: SlotComponentProps>; + input?: SlotComponentProps< + typeof TextField, + {}, + MultiInputTimeRangeFieldOwnerState & { position: 'start' | 'end' } + >; + separator?: SlotComponentProps>; +} + +export type UseMultiInputTimeRangeFieldDefaultizedProps = + UseTimeRangeFieldDefaultizedProps; diff --git a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/index.ts b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/index.ts new file mode 100644 index 0000000000000..4de66891b6eb8 --- /dev/null +++ b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/index.ts @@ -0,0 +1,2 @@ +export { MultiInputTimeRangeField as Unstable_MultiInputTimeRangeField } from './MultiInputTimeRangeField'; +export type { UseMultiInputTimeRangeFieldProps } from './MultiInputTimeRangeField.types'; diff --git a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/tests/MultiInputTimeRangeField.conformance.test.tsx b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/tests/MultiInputTimeRangeField.conformance.test.tsx new file mode 100644 index 0000000000000..f6ddfe2e14f79 --- /dev/null +++ b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/tests/MultiInputTimeRangeField.conformance.test.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { describeConformance } from '@mui/monorepo/test/utils'; +import { Unstable_MultiInputTimeRangeField as MultiInputTimeRangeField } from '@mui/x-date-pickers-pro/MultiInputTimeRangeField'; +import { createPickerRenderer, wrapPickerMount } from 'test/utils/pickers-utils'; + +describe('', () => { + const { render } = createPickerRenderer(); + + describeConformance(, () => ({ + classes: {}, + inheritComponent: 'div', + render, + muiName: 'MuiMultiInputTimeRangeField', + wrapMount: wrapPickerMount, + refInstanceof: window.HTMLDivElement, + // cannot test reactTestRenderer because of required context + skip: ['reactTestRenderer', 'themeVariants'], + })); +}); diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.interfaces.ts b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.interfaces.ts index 7c6054409bf10..aa9ca94ac22e3 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.interfaces.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.interfaces.ts @@ -1,18 +1,7 @@ import { TextFieldProps } from '@mui/material/TextField'; -import { BaseDateValidationProps, DefaultizedProps } from '@mui/x-date-pickers/internals'; -import { UseFieldInternalProps, FieldSection } from '@mui/x-date-pickers/internals-fields'; -import { DateRange, DayRangeValidationProps } from '../internal/models'; -import { DateRangeValidationError } from '../internal/hooks/validation/useDateRangeValidation'; +import { UseDateRangeFieldProps } from '../internal/models'; -export interface UseSingleInputDateRangeFieldProps - extends UseFieldInternalProps, DateRangeValidationError>, - DayRangeValidationProps, - BaseDateValidationProps {} - -export type UseSingleInputDateRangeFieldDefaultizedProps = DefaultizedProps< - UseSingleInputDateRangeFieldProps, - 'minDate' | 'maxDate' | 'disableFuture' | 'disablePast' ->; +export interface UseSingleInputDateRangeFieldProps extends UseDateRangeFieldProps {} export type UseSingleInputDateRangeFieldComponentProps = Omit< ChildProps, @@ -24,7 +13,3 @@ export type SingleInputDateRangeFieldProps = UseSingleInputDateRangeField TDate, TextFieldProps >; - -export interface DateRangeFieldSection extends FieldSection { - dateName: 'start' | 'end'; -} diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts index 843df54680bde..68396ae0db9c2 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts @@ -1,29 +1,17 @@ import * as React from 'react'; import { SlotComponentProps } from '@mui/base/utils'; import TextField, { TextFieldProps } from '@mui/material/TextField'; -import { - BaseDateValidationProps, - DefaultizedProps, - MakeOptional, -} from '@mui/x-date-pickers/internals'; -import { UseFieldInternalProps, FieldSection } from '@mui/x-date-pickers/internals-fields'; -import { DateRange, DayRangeValidationProps } from '../internal/models'; -import { DateRangeValidationError } from '../internal/hooks/validation/useDateRangeValidation'; +import { UseDateRangeFieldDefaultizedProps, UseDateRangeFieldProps } from '../internal/models'; export interface UseSingleInputDateRangeFieldParams { props: UseSingleInputDateRangeFieldComponentProps; inputRef?: React.Ref; } -export interface UseSingleInputDateRangeFieldProps - extends MakeOptional, DateRangeValidationError>, 'format'>, - DayRangeValidationProps, - BaseDateValidationProps {} +export interface UseSingleInputDateRangeFieldProps extends UseDateRangeFieldProps {} -export type UseSingleInputDateRangeFieldDefaultizedProps = DefaultizedProps< - UseSingleInputDateRangeFieldProps, - keyof BaseDateValidationProps | 'format' ->; +export type UseSingleInputDateRangeFieldDefaultizedProps = + UseDateRangeFieldDefaultizedProps; export type UseSingleInputDateRangeFieldComponentProps = Omit< TChildProps, @@ -58,7 +46,3 @@ export interface SingleInputDateRangeFieldSlotsComponent { export interface SingleInputDateRangeFieldSlotsComponentsProps { input?: SlotComponentProps>; } - -export interface DateRangeFieldSection extends FieldSection { - dateName: 'start' | 'end'; -} diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.utils.ts b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.utils.ts index 176a3b18a0ae4..016638f6eb1cb 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.utils.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.utils.ts @@ -1,4 +1,4 @@ -import { DateRangeFieldSection } from './SingleInputDateRangeField.types'; +import { DateRangeFieldSection } from '../internal/models'; export const splitDateRangeSections = (sections: DateRangeFieldSection[]) => { const startDateSections: DateRangeFieldSection[] = []; diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts index d5d7f0050508e..081d71cbc80ff 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/useSingleInputDateRangeField.ts @@ -1,139 +1,13 @@ import { useUtils, useDefaultDates, applyDefaultDate } from '@mui/x-date-pickers/internals'; +import { useField } from '@mui/x-date-pickers/internals-fields'; import { - useField, - FieldValueManager, - splitFormatIntoSections, - addPositionPropertiesToSections, - createDateStrFromSections, -} from '@mui/x-date-pickers/internals-fields'; -import { - DateRangeFieldSection, UseSingleInputDateRangeFieldDefaultizedProps, UseSingleInputDateRangeFieldParams, UseSingleInputDateRangeFieldProps, } from './SingleInputDateRangeField.types'; import { dateRangePickerValueManager } from '../DateRangePicker/shared'; -import { DateRange } from '../internal/models'; -import { splitDateRangeSections, removeLastSeparator } from './SingleInputDateRangeField.utils'; -import { - DateRangeValidationError, - isSameDateRangeError, - validateDateRange, -} from '../internal/hooks/validation/useDateRangeValidation'; - -export const dateRangeFieldValueManager: FieldValueManager< - DateRange, - any, - DateRangeFieldSection, - DateRangeValidationError -> = { - updateReferenceValue: (utils, value, prevReferenceValue) => { - const shouldKeepStartDate = value[0] != null && utils.isValid(value[0]); - const shouldKeepEndDate = value[1] != null && utils.isValid(value[1]); - - if (!shouldKeepStartDate && !shouldKeepEndDate) { - return prevReferenceValue; - } - - if (shouldKeepStartDate && shouldKeepEndDate) { - return value; - } - - if (shouldKeepStartDate) { - return [value[0], prevReferenceValue[0]]; - } - - return [prevReferenceValue[1], value[1]]; - }, - getSectionsFromValue: (utils, localeText, prevSections, [start, end], format) => { - const prevDateRangeSections = - prevSections == null - ? { startDate: null, endDate: null } - : splitDateRangeSections(prevSections); - - const getSections = (newDate: any | null, prevDateSections: DateRangeFieldSection[] | null) => { - const shouldReUsePrevDateSections = !utils.isValid(newDate) && !!prevDateSections; - - if (shouldReUsePrevDateSections) { - return prevDateSections; - } - - return splitFormatIntoSections(utils, localeText, format, newDate); - }; - - const rawSectionsOfStartDate = getSections(start, prevDateRangeSections.startDate); - const rawSectionsOfEndDate = getSections(end, prevDateRangeSections.endDate); - - const sectionsOfStartDate = rawSectionsOfStartDate.map((section, sectionIndex) => { - if (sectionIndex === rawSectionsOfStartDate.length - 1) { - return { - ...section, - dateName: 'start' as const, - separator: ' – ', - }; - } - - return { - ...section, - dateName: 'start' as const, - }; - }); - - const sectionsOfEndDate = rawSectionsOfEndDate.map((section) => ({ - ...section, - dateName: 'end' as const, - })); - - return addPositionPropertiesToSections([...sectionsOfStartDate, ...sectionsOfEndDate]); - }, - getValueStrFromSections: (sections) => { - const dateRangeSections = splitDateRangeSections(sections); - const startDateStr = createDateStrFromSections(dateRangeSections.startDate, true); - const endDateStr = createDateStrFromSections(dateRangeSections.endDate, true); - - return `${startDateStr}${endDateStr}`; - }, - getActiveDateSections: (sections, activeSection) => { - const index = activeSection.dateName === 'start' ? 0 : 1; - const dateRangeSections = splitDateRangeSections(sections); - - return index === 0 - ? removeLastSeparator(dateRangeSections.startDate) - : dateRangeSections.endDate; - }, - getActiveDateManager: (utils, state, activeSection) => { - const index = activeSection.dateName === 'start' ? 0 : 1; - - const updateDateInRange = (newDate: any, prevDateRange: DateRange) => - (index === 0 ? [newDate, prevDateRange[1]] : [prevDateRange[0], newDate]) as DateRange; - - return { - activeDate: state.value[index], - referenceActiveDate: state.referenceValue[index], - getNewValueFromNewActiveDate: (newActiveDate) => ({ - value: updateDateInRange(newActiveDate, state.value), - referenceValue: - newActiveDate == null || !utils.isValid(newActiveDate) - ? state.referenceValue - : updateDateInRange(newActiveDate, state.referenceValue), - }), - }; - }, - parseValueStr: (valueStr, referenceValue, parseDate) => { - // TODO: Improve because it would not work if the date format has `–` as a separator. - const [startStr, endStr] = valueStr.split('–'); - - return [startStr, endStr].map((dateStr, index) => { - if (dateStr == null) { - return null; - } - - return parseDate(dateStr.trim(), referenceValue[index]); - }) as DateRange; - }, - hasError: (error) => error[0] != null || error[1] != null, - isSameError: isSameDateRangeError, -}; +import { validateDateRange } from '../internal/hooks/validation/useDateRangeValidation'; +import { dateRangeFieldValueManager } from '../internal/hooks/valueManager/dateRangeValueManager'; export const useDefaultizedDateRangeFieldProps = ( props: UseSingleInputDateRangeFieldProps, diff --git a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/useMultiInputDateRangeField.ts b/packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts similarity index 77% rename from packages/x-date-pickers-pro/src/MultiInputDateRangeField/useMultiInputDateRangeField.ts rename to packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts index 531d86b4315b3..67c2731995932 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/useMultiInputDateRangeField.ts +++ b/packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts @@ -5,14 +5,16 @@ import { UseDateFieldComponentProps, } from '@mui/x-date-pickers/DateField'; import { useValidation } from '@mui/x-date-pickers/internals'; -import { DateRange } from '../internal/models'; -import { validateDateRange } from '../internal/hooks/validation/useDateRangeValidation'; -import { dateRangePickerValueManager } from '../DateRangePicker/shared'; +import { useDefaultizedDateRangeFieldProps } from '../../../SingleInputDateRangeField/useSingleInputDateRangeField'; +import { UseMultiInputDateRangeFieldParams } from '../../../MultiInputDateRangeField/MultiInputDateRangeField.types'; +import { DateRange } from '../../models/range'; import { - dateRangeFieldValueManager, - useDefaultizedDateRangeFieldProps, -} from '../SingleInputDateRangeField/useSingleInputDateRangeField'; -import { UseMultiInputDateRangeFieldParams } from './MultiInputDateRangeField.types'; + DateRangeComponentValidationProps, + DateRangeValidationError, + validateDateRange, +} from '../validation/useDateRangeValidation'; +import { dateRangePickerValueManager } from '../../../DateRangePicker/shared'; +import { dateRangeFieldValueManager } from '../valueManager/dateRangeValueManager'; export const useMultiInputDateRangeField = ({ sharedProps: inSharedProps, @@ -73,7 +75,12 @@ export const useMultiInputDateRangeField = ({ const value = valueProp ?? firstDefaultValue.current ?? dateRangePickerValueManager.emptyValue; - const validationError = useValidation({ ...sharedProps, value }, validateDateRange, () => true); + const validationError = useValidation< + DateRange, + TDate, + DateRangeValidationError, + DateRangeComponentValidationProps + >({ ...sharedProps, value }, validateDateRange, () => true); const inputError = React.useMemo( () => dateRangeFieldValueManager.hasError(validationError), [validationError], diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts b/packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts new file mode 100644 index 0000000000000..ab5c640c26498 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts @@ -0,0 +1,132 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { + unstable_useDateTimeField as useDateTimeField, + UseDateTimeFieldComponentProps, +} from '@mui/x-date-pickers/DateTimeField'; +import { + applyDefaultDate, + useDefaultDates, + useUtils, + useValidation, +} from '@mui/x-date-pickers/internals'; +import { DateRange } from '../../models/range'; +import { + UseMultiInputDateTimeRangeFieldDefaultizedProps, + UseMultiInputDateTimeRangeFieldParams, + UseMultiInputDateTimeRangeFieldProps, +} from '../../../MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types'; +import { + DateTimeRangeComponentValidationProps, + DateTimeRangeValidationError, + validateDateTimeRange, +} from '../validation/useDateTimeRangeValidation'; +import { dateRangePickerValueManager } from '../../../DateRangePicker/shared'; +import { dateTimeRangeFieldValueManager } from '../valueManager/dateTimeRangeValueManager'; + +export const useDefaultizedDateTimeRangeFieldProps = ( + props: UseMultiInputDateTimeRangeFieldProps, +): UseMultiInputDateTimeRangeFieldDefaultizedProps & AdditionalProps => { + const utils = useUtils(); + const defaultDates = useDefaultDates(); + + const ampm = props.ampm ?? utils.is12HourCycleInCurrentLocale(); + const defaultFormat = ampm + ? utils.formats.keyboardDateTime12h + : utils.formats.keyboardDateTime24h; + + return { + ...props, + disablePast: props.disablePast ?? false, + disableFuture: props.disableFuture ?? false, + format: props.format ?? defaultFormat, + minDate: applyDefaultDate(utils, props.minDateTime ?? props.minDate, defaultDates.minDate), + maxDate: applyDefaultDate(utils, props.maxDateTime ?? props.maxDate, defaultDates.maxDate), + minTime: props.minDateTime ?? props.minTime, + maxTime: props.maxDateTime ?? props.maxTime, + disableIgnoringDatePartForTimeValidation: Boolean(props.minDateTime || props.maxDateTime), + } as any; +}; + +export const useMultiInputDateTimeRangeField = ({ + sharedProps: inSharedProps, + startInputProps: inStartInputProps, + endInputProps: inEndInputProps, + startInputRef, + endInputRef, +}: UseMultiInputDateTimeRangeFieldParams) => { + const sharedProps = useDefaultizedDateTimeRangeFieldProps(inSharedProps); + + const { value: valueProp, defaultValue, format, onChange } = sharedProps; + + const firstDefaultValue = React.useRef(defaultValue); + + // TODO: Maybe export utility from `useField` instead of copy/pasting the logic + const buildChangeHandler = (index: 0 | 1) => { + if (!onChange) { + return () => {}; + } + + return (newDate: TDate | null) => { + const currentDateRange = + valueProp ?? firstDefaultValue.current ?? dateRangePickerValueManager.emptyValue; + const newDateRange: DateRange = + index === 0 ? [newDate, currentDateRange[1]] : [currentDateRange[0], newDate]; + + onChange(newDateRange); + }; + }; + + const handleStartDateChange = useEventCallback(buildChangeHandler(0)); + const handleEndDateChange = useEventCallback(buildChangeHandler(1)); + + const startInputProps: UseDateTimeFieldComponentProps = { + ...inStartInputProps, + format, + value: valueProp === undefined ? undefined : valueProp[0], + defaultValue: defaultValue === undefined ? undefined : defaultValue[0], + onChange: handleStartDateChange, + }; + + const endInputProps: UseDateTimeFieldComponentProps = { + ...inEndInputProps, + format, + value: valueProp === undefined ? undefined : valueProp[1], + defaultValue: defaultValue === undefined ? undefined : defaultValue[1], + onChange: handleEndDateChange, + }; + + const rawStartDateResponse = useDateTimeField({ + props: startInputProps, + inputRef: startInputRef, + }); + const rawEndDateResponse = useDateTimeField({ + props: endInputProps, + inputRef: endInputRef, + }); + + const value = valueProp ?? firstDefaultValue.current ?? dateRangePickerValueManager.emptyValue; + + const validationError = useValidation< + DateRange, + TDate, + DateTimeRangeValidationError, + DateTimeRangeComponentValidationProps + >({ ...sharedProps, value }, validateDateTimeRange, () => true); + const inputError = React.useMemo( + () => dateTimeRangeFieldValueManager.hasError(validationError), + [validationError], + ); + + const startDateResponse = { + ...rawStartDateResponse, + error: inputError, + }; + + const endDateResponse = { + ...rawEndDateResponse, + error: inputError, + }; + + return { startDate: startDateResponse, endDate: endDateResponse }; +}; diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts b/packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts new file mode 100644 index 0000000000000..70de596fe9d06 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts @@ -0,0 +1,121 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import { + unstable_useTimeField as useTimeField, + UseTimeFieldComponentProps, +} from '@mui/x-date-pickers/TimeField'; +import { useUtils, useValidation } from '@mui/x-date-pickers/internals'; +import { DateRange } from '../../models/range'; +import { + TimeRangeValidationError, + TimeRangeComponentValidationProps, + validateTimeRange, +} from '../validation/useTimeRangeValidation'; +import { + UseMultiInputTimeRangeFieldDefaultizedProps, + UseMultiInputTimeRangeFieldParams, + UseMultiInputTimeRangeFieldProps, +} from '../../../MultiInputTimeRangeField/MultiInputTimeRangeField.types'; +import { dateRangePickerValueManager } from '../../../DateRangePicker/shared'; +import { timeRangeFieldValueManager } from '../valueManager/timeRangeValueManager'; + +export const useDefaultizedTimeRangeFieldProps = ( + props: UseMultiInputTimeRangeFieldProps, +): UseMultiInputTimeRangeFieldDefaultizedProps & AdditionalProps => { + const utils = useUtils(); + + const ampm = props.ampm ?? utils.is12HourCycleInCurrentLocale(); + const defaultFormat = ampm ? utils.formats.fullTime12h : utils.formats.fullTime24h; + + return { + ...props, + disablePast: props.disablePast ?? false, + disableFuture: props.disableFuture ?? false, + format: props.format ?? defaultFormat, + minTime: props.minTime, + maxTime: props.maxTime, + } as any; +}; + +export const useMultiInputTimeRangeField = ({ + sharedProps: inSharedProps, + startInputProps: inStartInputProps, + endInputProps: inEndInputProps, + startInputRef, + endInputRef, +}: UseMultiInputTimeRangeFieldParams) => { + const sharedProps = useDefaultizedTimeRangeFieldProps(inSharedProps); + + const { value: valueProp, defaultValue, format, onChange } = sharedProps; + + const firstDefaultValue = React.useRef(defaultValue); + + // TODO: Maybe export utility from `useField` instead of copy/pasting the logic + const buildChangeHandler = (index: 0 | 1) => { + if (!onChange) { + return () => {}; + } + + return (newDate: TDate | null) => { + const currentDateRange = + valueProp ?? firstDefaultValue.current ?? dateRangePickerValueManager.emptyValue; + const newDateRange: DateRange = + index === 0 ? [newDate, currentDateRange[1]] : [currentDateRange[0], newDate]; + + onChange(newDateRange); + }; + }; + + const handleStartDateChange = useEventCallback(buildChangeHandler(0)); + const handleEndDateChange = useEventCallback(buildChangeHandler(1)); + + const startInputProps: UseTimeFieldComponentProps = { + ...inStartInputProps, + format, + value: valueProp === undefined ? undefined : valueProp[0], + defaultValue: defaultValue === undefined ? undefined : defaultValue[0], + onChange: handleStartDateChange, + }; + + const endInputProps: UseTimeFieldComponentProps = { + ...inEndInputProps, + format, + value: valueProp === undefined ? undefined : valueProp[1], + defaultValue: defaultValue === undefined ? undefined : defaultValue[1], + onChange: handleEndDateChange, + }; + + const rawStartDateResponse = useTimeField({ + props: startInputProps, + inputRef: startInputRef, + }); + const rawEndDateResponse = useTimeField({ + props: endInputProps, + inputRef: endInputRef, + }); + + const value = valueProp ?? firstDefaultValue.current ?? dateRangePickerValueManager.emptyValue; + + const validationError = useValidation< + DateRange, + TDate, + TimeRangeValidationError, + TimeRangeComponentValidationProps + >({ ...sharedProps, value }, validateTimeRange, () => true); + const inputError = React.useMemo( + () => timeRangeFieldValueManager.hasError(validationError), + [validationError], + ); + + const startDateResponse = { + ...rawStartDateResponse, + error: inputError, + }; + + const endDateResponse = { + ...rawEndDateResponse, + error: inputError, + }; + + return { startDate: startDateResponse, endDate: endDateResponse }; +}; diff --git a/packages/x-date-pickers-pro/src/internal/hooks/validation/useDateRangeValidation.ts b/packages/x-date-pickers-pro/src/internal/hooks/validation/useDateRangeValidation.ts index f6e8033901004..cb7ce55c07c14 100644 --- a/packages/x-date-pickers-pro/src/internal/hooks/validation/useDateRangeValidation.ts +++ b/packages/x-date-pickers-pro/src/internal/hooks/validation/useDateRangeValidation.ts @@ -7,7 +7,7 @@ import { ValidationProps, } from '@mui/x-date-pickers/internals'; import { isRangeValid } from '../../utils/date-utils'; -import { DateRange, DayRangeValidationProps } from '../../models/dateRange'; +import { DateRange, DayRangeValidationProps } from '../../models'; export interface DateRangeComponentValidationProps extends DayRangeValidationProps, diff --git a/packages/x-date-pickers-pro/src/internal/hooks/validation/useDateTimeRangeValidation.ts b/packages/x-date-pickers-pro/src/internal/hooks/validation/useDateTimeRangeValidation.ts new file mode 100644 index 0000000000000..1d342f9f0a9f9 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internal/hooks/validation/useDateTimeRangeValidation.ts @@ -0,0 +1,87 @@ +import { + useValidation, + Validator, + DateTimeValidationError, + validateDateTime, + BaseDateValidationProps, + TimeValidationProps, + ValidationProps, +} from '@mui/x-date-pickers/internals'; +import { isRangeValid } from '../../utils/date-utils'; +import { DayRangeValidationProps } from '../../models/dateRange'; +import { DateRange } from '../../models/range'; + +export interface DateTimeRangeComponentValidationProps + extends DayRangeValidationProps, + TimeValidationProps, + Required> {} + +export const validateDateTimeRange: Validator< + DateRange, + any, + DateTimeRangeValidationError, + DateTimeRangeComponentValidationProps +> = ({ props, value, adapter }) => { + const [start, end] = value; + + // for partial input + if (start === null || end === null) { + return [null, null]; + } + + const { shouldDisableDate, ...otherProps } = props; + + const dateTimeValidations: [ + DateTimeRangeValidationErrorValue, + DateTimeRangeValidationErrorValue, + ] = [ + validateDateTime({ + adapter, + value: start, + props: { + ...otherProps, + shouldDisableDate: (day) => !!shouldDisableDate?.(day, 'start'), + }, + }), + validateDateTime({ + adapter, + value: end, + props: { + ...otherProps, + shouldDisableDate: (day) => !!shouldDisableDate?.(day, 'end'), + }, + }), + ]; + + if (dateTimeValidations[0] || dateTimeValidations[1]) { + return dateTimeValidations; + } + + if (!isRangeValid(adapter.utils, value)) { + return ['invalidRange', 'invalidRange']; + } + + return [null, null]; +}; + +type DateTimeRangeValidationErrorValue = DateTimeValidationError | 'invalidRange' | null; + +export type DateTimeRangeValidationError = [ + DateTimeRangeValidationErrorValue, + DateTimeRangeValidationErrorValue, +]; + +export const isSameDateTimeRangeError = ( + a: DateTimeRangeValidationError, + b: DateTimeRangeValidationError | null, +) => b !== null && a[1] === b[1] && a[0] === b[0]; + +export const useDateRangeValidation = ( + props: ValidationProps< + DateTimeRangeValidationError, + DateRange, + DateTimeRangeComponentValidationProps + >, +): DateTimeRangeValidationError => { + return useValidation(props, validateDateTimeRange, isSameDateTimeRangeError); +}; diff --git a/packages/x-date-pickers-pro/src/internal/hooks/validation/useTimeRangeValidation.ts b/packages/x-date-pickers-pro/src/internal/hooks/validation/useTimeRangeValidation.ts new file mode 100644 index 0000000000000..4a4e2aa9e2306 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internal/hooks/validation/useTimeRangeValidation.ts @@ -0,0 +1,71 @@ +import { + useValidation, + Validator, + TimeValidationError, + validateTime, + BaseTimeValidationProps, + ValidationProps, +} from '@mui/x-date-pickers/internals'; +import { isRangeValid } from '../../utils/date-utils'; +import { DateRange } from '../../models/range'; + +export interface TimeRangeComponentValidationProps extends Required {} + +export const validateTimeRange: Validator< + DateRange, + any, + TimeRangeValidationError, + TimeRangeComponentValidationProps +> = ({ props, value, adapter }) => { + const [start, end] = value; + + // for partial input + if (start === null || end === null) { + return [null, null]; + } + + const dateTimeValidations: [TimeRangeValidationErrorValue, TimeRangeValidationErrorValue] = [ + validateTime({ + adapter, + value: start, + props, + }), + validateTime({ + adapter, + value: end, + props, + }), + ]; + + if (dateTimeValidations[0] || dateTimeValidations[1]) { + return dateTimeValidations; + } + + if (!isRangeValid(adapter.utils, value)) { + return ['invalidRange', 'invalidRange']; + } + + return [null, null]; +}; + +type TimeRangeValidationErrorValue = TimeValidationError | 'invalidRange' | null; + +export type TimeRangeValidationError = [ + TimeRangeValidationErrorValue, + TimeRangeValidationErrorValue, +]; + +export const isSameTimeRangeError = ( + a: TimeRangeValidationError, + b: TimeRangeValidationError | null, +) => b !== null && a[1] === b[1] && a[0] === b[0]; + +export const useDateRangeValidation = ( + props: ValidationProps< + TimeRangeValidationError, + DateRange, + TimeRangeComponentValidationProps + >, +): TimeRangeValidationError => { + return useValidation(props, validateTimeRange, isSameTimeRangeError); +}; diff --git a/packages/x-date-pickers-pro/src/internal/hooks/valueManager/common.ts b/packages/x-date-pickers-pro/src/internal/hooks/valueManager/common.ts new file mode 100644 index 0000000000000..2f1297bed6d4b --- /dev/null +++ b/packages/x-date-pickers-pro/src/internal/hooks/valueManager/common.ts @@ -0,0 +1,119 @@ +import { + FieldValueManager, + splitFormatIntoSections, + addPositionPropertiesToSections, + createDateStrFromSections, +} from '@mui/x-date-pickers/internals-fields'; +import { DateRange, DateRangeFieldSection } from '../../models/range'; +import { splitDateRangeSections, removeLastSeparator } from '../../utils/date-fields-utils'; + +export const rangeFieldValueManager: Omit< + FieldValueManager, any, DateRangeFieldSection, any>, + 'isSameError' +> = { + updateReferenceValue: (utils, value, prevReferenceValue) => { + const shouldKeepStartDate = value[0] != null && utils.isValid(value[0]); + const shouldKeepEndDate = value[1] != null && utils.isValid(value[1]); + + if (!shouldKeepStartDate && !shouldKeepEndDate) { + return prevReferenceValue; + } + + if (shouldKeepStartDate && shouldKeepEndDate) { + return value; + } + + if (shouldKeepStartDate) { + return [value[0], prevReferenceValue[0]]; + } + + return [prevReferenceValue[1], value[1]]; + }, + getSectionsFromValue: (utils, localeText, prevSections, [start, end], format) => { + const prevDateRangeSections = + prevSections == null + ? { startDate: null, endDate: null } + : splitDateRangeSections(prevSections); + + const getSections = (newDate: any | null, prevDateSections: DateRangeFieldSection[] | null) => { + const shouldReUsePrevDateSections = !utils.isValid(newDate) && !!prevDateSections; + + if (shouldReUsePrevDateSections) { + return prevDateSections; + } + + return splitFormatIntoSections(utils, localeText, format, newDate); + }; + + const rawSectionsOfStartDate = getSections(start, prevDateRangeSections.startDate); + const rawSectionsOfEndDate = getSections(end, prevDateRangeSections.endDate); + + const sectionsOfStartDate = rawSectionsOfStartDate.map((section, sectionIndex) => { + if (sectionIndex === rawSectionsOfStartDate.length - 1) { + return { + ...section, + dateName: 'start' as const, + separator: ' – ', + }; + } + + return { + ...section, + dateName: 'start' as const, + }; + }); + + const sectionsOfEndDate = rawSectionsOfEndDate.map((section) => ({ + ...section, + dateName: 'end' as const, + })); + + return addPositionPropertiesToSections([...sectionsOfStartDate, ...sectionsOfEndDate]); + }, + getValueStrFromSections: (sections) => { + const dateRangeSections = splitDateRangeSections(sections); + const startDateStr = createDateStrFromSections(dateRangeSections.startDate, true); + const endDateStr = createDateStrFromSections(dateRangeSections.endDate, true); + + return `${startDateStr}${endDateStr}`; + }, + getActiveDateSections: (sections, activeSection) => { + const index = activeSection.dateName === 'start' ? 0 : 1; + const dateRangeSections = splitDateRangeSections(sections); + + return index === 0 + ? removeLastSeparator(dateRangeSections.startDate) + : dateRangeSections.endDate; + }, + parseValueStr: (valueStr, referenceValue, parseDate) => { + // TODO: Improve because it would not work if the date format has `–` as a separator. + const [startStr, endStr] = valueStr.split('–'); + + return [startStr, endStr].map((dateStr, index) => { + if (dateStr == null) { + return null; + } + + return parseDate(dateStr.trim(), referenceValue[index]); + }) as DateRange; + }, + getActiveDateManager: (utils, state, activeSection) => { + const index = activeSection.dateName === 'start' ? 0 : 1; + + const updateDateInRange = (newDate: any, prevDateRange: DateRange) => + (index === 0 ? [newDate, prevDateRange[1]] : [prevDateRange[0], newDate]) as DateRange; + + return { + activeDate: state.value[index], + referenceActiveDate: state.referenceValue[index], + getNewValueFromNewActiveDate: (newActiveDate) => ({ + value: updateDateInRange(newActiveDate, state.value), + referenceValue: + newActiveDate == null || !utils.isValid(newActiveDate) + ? state.referenceValue + : updateDateInRange(newActiveDate, state.referenceValue), + }), + }; + }, + hasError: (error) => error[0] != null || error[1] != null, +}; diff --git a/packages/x-date-pickers-pro/src/internal/hooks/valueManager/dateRangeValueManager.ts b/packages/x-date-pickers-pro/src/internal/hooks/valueManager/dateRangeValueManager.ts new file mode 100644 index 0000000000000..d45181a450c70 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internal/hooks/valueManager/dateRangeValueManager.ts @@ -0,0 +1,17 @@ +import { FieldValueManager } from '@mui/x-date-pickers/internals-fields'; +import { DateRange, DateRangeFieldSection } from '../../models/range'; +import { + DateRangeValidationError, + isSameDateRangeError, +} from '../validation/useDateRangeValidation'; +import { rangeFieldValueManager } from './common'; + +export const dateRangeFieldValueManager: FieldValueManager< + DateRange, + any, + DateRangeFieldSection, + DateRangeValidationError +> = { + ...rangeFieldValueManager, + isSameError: isSameDateRangeError, +}; diff --git a/packages/x-date-pickers-pro/src/internal/hooks/valueManager/dateTimeRangeValueManager.ts b/packages/x-date-pickers-pro/src/internal/hooks/valueManager/dateTimeRangeValueManager.ts new file mode 100644 index 0000000000000..19aed27d7c1ab --- /dev/null +++ b/packages/x-date-pickers-pro/src/internal/hooks/valueManager/dateTimeRangeValueManager.ts @@ -0,0 +1,17 @@ +import { FieldValueManager } from '@mui/x-date-pickers/internals-fields'; +import { DateRange, DateRangeFieldSection } from '../../models/range'; +import { + DateTimeRangeValidationError, + isSameDateTimeRangeError, +} from '../validation/useDateTimeRangeValidation'; +import { rangeFieldValueManager } from './common'; + +export const dateTimeRangeFieldValueManager: FieldValueManager< + DateRange, + any, + DateRangeFieldSection, + DateTimeRangeValidationError +> = { + ...rangeFieldValueManager, + isSameError: isSameDateTimeRangeError, +}; diff --git a/packages/x-date-pickers-pro/src/internal/hooks/valueManager/timeRangeValueManager.ts b/packages/x-date-pickers-pro/src/internal/hooks/valueManager/timeRangeValueManager.ts new file mode 100644 index 0000000000000..76a1cd1827b9a --- /dev/null +++ b/packages/x-date-pickers-pro/src/internal/hooks/valueManager/timeRangeValueManager.ts @@ -0,0 +1,17 @@ +import { FieldValueManager } from '@mui/x-date-pickers/internals-fields'; +import { DateRange, DateRangeFieldSection } from '../../models/range'; +import { + TimeRangeValidationError, + isSameTimeRangeError, +} from '../validation/useTimeRangeValidation'; +import { rangeFieldValueManager } from './common'; + +export const timeRangeFieldValueManager: FieldValueManager< + DateRange, + any, + DateRangeFieldSection, + TimeRangeValidationError +> = { + ...rangeFieldValueManager, + isSameError: isSameTimeRangeError, +}; diff --git a/packages/x-date-pickers-pro/src/internal/models/dateRange.ts b/packages/x-date-pickers-pro/src/internal/models/dateRange.ts index 1b5245c548746..dfe4406ee85bf 100644 --- a/packages/x-date-pickers-pro/src/internal/models/dateRange.ts +++ b/packages/x-date-pickers-pro/src/internal/models/dateRange.ts @@ -1,10 +1,11 @@ -export type DateRange = [TDate | null, TDate | null]; -export type NonEmptyDateRange = [TDate, TDate]; - -export interface CurrentlySelectingRangeEndProps { - currentlySelectingRangeEnd: 'start' | 'end'; - setCurrentlySelectingRangeEnd: (newSelectingEnd: 'start' | 'end') => void; -} +import { + BaseDateValidationProps, + DefaultizedProps, + MakeOptional, +} from '@mui/x-date-pickers/internals'; +import { UseFieldInternalProps } from '@mui/x-date-pickers/internals-fields'; +import { DateRange } from './range'; +import type { DateRangeValidationError } from '../hooks/validation/useDateRangeValidation'; /** * Props used to validate a day value in range pickers. @@ -19,3 +20,13 @@ export interface DayRangeValidationProps { */ shouldDisableDate?: (day: TDate, position: 'start' | 'end') => boolean; } + +export interface UseDateRangeFieldProps + extends MakeOptional, DateRangeValidationError>, 'format'>, + DayRangeValidationProps, + BaseDateValidationProps {} + +export type UseDateRangeFieldDefaultizedProps = DefaultizedProps< + UseDateRangeFieldProps, + keyof BaseDateValidationProps | 'format' +>; diff --git a/packages/x-date-pickers-pro/src/internal/models/dateTimeRange.ts b/packages/x-date-pickers-pro/src/internal/models/dateTimeRange.ts new file mode 100644 index 0000000000000..b7b56f77ca056 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internal/models/dateTimeRange.ts @@ -0,0 +1,38 @@ +import { UseFieldInternalProps } from '@mui/x-date-pickers/internals-fields'; +import { + BaseDateValidationProps, + TimeValidationProps, + DefaultizedProps, + MakeOptional, +} from '@mui/x-date-pickers/internals'; +import { DayRangeValidationProps } from './dateRange'; +import { DateRange } from './range'; +import { DateTimeRangeValidationError } from '../hooks/validation/useDateTimeRangeValidation'; + +export interface UseDateTimeRangeFieldProps + extends MakeOptional< + UseFieldInternalProps, DateTimeRangeValidationError>, + 'format' + >, + DayRangeValidationProps, + TimeValidationProps, + BaseDateValidationProps { + /** + * Minimal selectable moment of time with binding to date, to set min time in each day use `minTime`. + */ + minDateTime?: TDate; + /** + * Maximal selectable moment of time with binding to date, to set max time in each day use `maxTime`. + */ + maxDateTime?: TDate; + /** + * 12h/24h view for hour selection clock. + * @default `utils.is12HourCycleInCurrentLocale()` + */ + ampm?: boolean; +} + +export type UseDateTimeRangeFieldDefaultizedProps = DefaultizedProps< + UseDateTimeRangeFieldProps, + keyof BaseDateValidationProps | 'format' | 'disableIgnoringDatePartForTimeValidation' +>; diff --git a/packages/x-date-pickers-pro/src/internal/models/index.ts b/packages/x-date-pickers-pro/src/internal/models/index.ts index 0f4b8e3531d7b..61657566f5b7b 100644 --- a/packages/x-date-pickers-pro/src/internal/models/index.ts +++ b/packages/x-date-pickers-pro/src/internal/models/index.ts @@ -1 +1,4 @@ export * from './dateRange'; +export * from './range'; +export * from './dateTimeRange'; +export * from './timeRange'; diff --git a/packages/x-date-pickers-pro/src/internal/models/range.ts b/packages/x-date-pickers-pro/src/internal/models/range.ts new file mode 100644 index 0000000000000..935347e5d2f42 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internal/models/range.ts @@ -0,0 +1,13 @@ +import { FieldSection } from '@mui/x-date-pickers/internals-fields'; + +export type DateRange = [TDate | null, TDate | null]; +export type NonEmptyDateRange = [TDate, TDate]; + +export interface CurrentlySelectingRangeEndProps { + currentlySelectingRangeEnd: 'start' | 'end'; + setCurrentlySelectingRangeEnd: (newSelectingEnd: 'start' | 'end') => void; +} + +export interface DateRangeFieldSection extends FieldSection { + dateName: 'start' | 'end'; +} diff --git a/packages/x-date-pickers-pro/src/internal/models/timeRange.ts b/packages/x-date-pickers-pro/src/internal/models/timeRange.ts new file mode 100644 index 0000000000000..afa4e759e391c --- /dev/null +++ b/packages/x-date-pickers-pro/src/internal/models/timeRange.ts @@ -0,0 +1,25 @@ +import { UseFieldInternalProps } from '@mui/x-date-pickers/internals-fields'; +import { + BaseTimeValidationProps, + TimeValidationProps, + DefaultizedProps, + MakeOptional, +} from '@mui/x-date-pickers/internals'; +import { DateRange } from './range'; +import { TimeRangeValidationError } from '../hooks/validation/useTimeRangeValidation'; + +export interface UseTimeRangeFieldProps + extends MakeOptional, TimeRangeValidationError>, 'format'>, + TimeValidationProps, + BaseTimeValidationProps { + /** + * 12h/24h view for hour selection clock. + * @default `utils.is12HourCycleInCurrentLocale()` + */ + ampm?: boolean; +} + +export type UseTimeRangeFieldDefaultizedProps = DefaultizedProps< + UseTimeRangeFieldProps, + keyof BaseTimeValidationProps | 'format' +>; diff --git a/packages/x-date-pickers-pro/src/internal/utils/date-fields-utils.ts b/packages/x-date-pickers-pro/src/internal/utils/date-fields-utils.ts new file mode 100644 index 0000000000000..cb4728715a7e2 --- /dev/null +++ b/packages/x-date-pickers-pro/src/internal/utils/date-fields-utils.ts @@ -0,0 +1,24 @@ +import { DateRangeFieldSection } from '../models/range'; + +export const splitDateRangeSections = (sections: DateRangeFieldSection[]) => { + const startDateSections: DateRangeFieldSection[] = []; + const endDateSections: DateRangeFieldSection[] = []; + sections.forEach((section) => { + if (section.dateName === 'start') { + startDateSections.push(section); + } else { + endDateSections.push(section); + } + }); + + return { startDate: startDateSections, endDate: endDateSections }; +}; + +export const removeLastSeparator = (dateSections: DateRangeFieldSection[]) => + dateSections.map((section, sectionIndex) => { + if (sectionIndex === dateSections.length - 1) { + return { ...section, separator: null }; + } + + return section; + }); diff --git a/packages/x-date-pickers-pro/src/internal/utils/date-utils.ts b/packages/x-date-pickers-pro/src/internal/utils/date-utils.ts index 366813f24c259..edb91ac14e8c4 100644 --- a/packages/x-date-pickers-pro/src/internal/utils/date-utils.ts +++ b/packages/x-date-pickers-pro/src/internal/utils/date-utils.ts @@ -1,5 +1,5 @@ import { MuiPickersAdapter, replaceInvalidDateByNull } from '@mui/x-date-pickers/internals'; -import { DateRange, NonEmptyDateRange } from '../models'; +import { DateRange, NonEmptyDateRange } from '../models/range'; export const replaceInvalidDatesByNull = ( utils: MuiPickersAdapter, diff --git a/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts b/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts index 038a48a86e964..5b4d31950af4f 100644 --- a/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts +++ b/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts @@ -26,6 +26,11 @@ export interface UseDateTimeFieldProps BaseDateValidationProps, TimeValidationProps, BaseTimeValidationProps { + /** + * 12h/24h view for hour selection clock. + * @default `utils.is12HourCycleInCurrentLocale()` + */ + ampm?: boolean; /** * Minimal selectable moment of time with binding to date, to set min time in each day use `minTime`. */ diff --git a/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts b/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts index 54efe71b996df..a3b0fe1f5dd59 100644 --- a/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts +++ b/packages/x-date-pickers/src/DateTimeField/useDateTimeField.ts @@ -31,11 +31,16 @@ const useDefaultizedDateTimeField = ( const utils = useUtils(); const defaultDates = useDefaultDates(); + const ampm = props.ampm ?? utils.is12HourCycleInCurrentLocale(); + const defaultFormat = ampm + ? utils.formats.keyboardDateTime12h + : utils.formats.keyboardDateTime24h; + return { ...props, disablePast: props.disablePast ?? false, disableFuture: props.disableFuture ?? false, - format: props.format ?? utils.formats.keyboardDateTime, + format: props.format ?? defaultFormat, disableIgnoringDatePartForTimeValidation: Boolean(props.minDateTime || props.maxDateTime), minDate: applyDefaultDate(utils, props.minDateTime ?? props.minDate, defaultDates.minDate), maxDate: applyDefaultDate(utils, props.maxDateTime ?? props.maxDate, defaultDates.maxDate), @@ -71,6 +76,7 @@ export const useDateTimeField = ({ shouldDisableTime, selectedSections, onSelectedSectionsChange, + ampm, ...other } = useDefaultizedDateTimeField(props); @@ -99,6 +105,7 @@ export const useDateTimeField = ({ selectedSections, onSelectedSectionsChange, inputRef, + ampm, }, valueManager: dateTimePickerValueManager, fieldValueManager: dateTimeFieldValueManager, diff --git a/packages/x-date-pickers/src/TimeField/TimeField.types.ts b/packages/x-date-pickers/src/TimeField/TimeField.types.ts index 3ea6d8f96f638..0fe98fe6f5ab2 100644 --- a/packages/x-date-pickers/src/TimeField/TimeField.types.ts +++ b/packages/x-date-pickers/src/TimeField/TimeField.types.ts @@ -14,7 +14,13 @@ export interface UseTimeFieldParams { export interface UseTimeFieldProps extends MakeOptional, 'format'>, TimeValidationProps, - BaseTimeValidationProps {} + BaseTimeValidationProps { + /** + * 12h/24h view for hour selection clock. + * @default `utils.is12HourCycleInCurrentLocale()` + */ + ampm?: boolean; +} export type UseTimeFieldDefaultizedProps = DefaultizedProps< UseTimeFieldProps, diff --git a/packages/x-date-pickers/src/TimeField/index.ts b/packages/x-date-pickers/src/TimeField/index.ts index 3c6e2bbb3d594..8705e11f26e53 100644 --- a/packages/x-date-pickers/src/TimeField/index.ts +++ b/packages/x-date-pickers/src/TimeField/index.ts @@ -1,5 +1,5 @@ export { TimeField as Unstable_TimeField } from './TimeField'; -export { useTimeField as unstable_useDateTimeField } from './useTimeField'; +export { useTimeField as unstable_useTimeField } from './useTimeField'; export type { UseTimeFieldProps, UseTimeFieldComponentProps, diff --git a/packages/x-date-pickers/src/TimeField/useTimeField.ts b/packages/x-date-pickers/src/TimeField/useTimeField.ts index 2ad8bd02c4684..e515988ac4006 100644 --- a/packages/x-date-pickers/src/TimeField/useTimeField.ts +++ b/packages/x-date-pickers/src/TimeField/useTimeField.ts @@ -24,11 +24,14 @@ const useDefaultizedTimeField = ( ): AdditionalProps & UseTimeFieldDefaultizedProps => { const utils = useUtils(); + const ampm = props.ampm ?? utils.is12HourCycleInCurrentLocale(); + const defaultFormat = ampm ? utils.formats.fullTime12h : utils.formats.fullTime24h; + return { ...props, disablePast: props.disablePast ?? false, disableFuture: props.disableFuture ?? false, - format: props.format ?? utils.formats.fullTime, + format: props.format ?? defaultFormat, } as any; }; @@ -52,6 +55,7 @@ export const useTimeField = ({ disableIgnoringDatePartForTimeValidation, selectedSections, onSelectedSectionsChange, + ampm, ...other } = useDefaultizedTimeField(props); @@ -75,6 +79,7 @@ export const useTimeField = ({ selectedSections, onSelectedSectionsChange, inputRef, + ampm, }, valueManager: timePickerValueManager, fieldValueManager: timeFieldValueManager, diff --git a/packages/x-date-pickers/src/internals/index.ts b/packages/x-date-pickers/src/internals/index.ts index 5557fb15cc191..aff77ab9e914c 100644 --- a/packages/x-date-pickers/src/internals/index.ts +++ b/packages/x-date-pickers/src/internals/index.ts @@ -82,7 +82,12 @@ export { usePickerState } from './hooks/usePickerState'; export type { PickerStateProps, PickerStatePickerProps } from './hooks/usePickerState'; export type { PickerStateValueManager, PickerSelectionState } from './hooks/usePickerState'; export { useLocalizationContext, useDefaultDates, useUtils, useLocaleText } from './hooks/useUtils'; -export type { BaseDateValidationProps, DayValidationProps } from './hooks/validation/models'; +export type { + BaseDateValidationProps, + BaseTimeValidationProps, + TimeValidationProps, + DayValidationProps, +} from './hooks/validation/models'; export { useValidation } from './hooks/validation/useValidation'; export type { ValidationCommonProps, @@ -90,7 +95,11 @@ export type { Validator, } from './hooks/validation/useValidation'; export { validateDate } from './hooks/validation/useDateValidation'; +export { validateTime } from './hooks/validation/useTimeValidation'; +export { validateDateTime } from './hooks/validation/useDateTimeValidation'; export type { DateValidationError } from './hooks/validation/useDateValidation'; +export type { TimeValidationError } from './hooks/validation/useTimeValidation'; +export type { DateTimeValidationError } from './hooks/validation/useDateTimeValidation'; export { usePreviousMonthDisabled, useNextMonthDisabled } from './hooks/date-helpers-hooks'; export type { BasePickerProps } from './models/props/basePickerProps';