diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx index e583975eccda1..a49e3bf17b8c6 100644 --- a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx @@ -23,7 +23,7 @@ import { MultiSectionDigitalClockViewProps, } from './MultiSectionDigitalClock.types'; import { getHourSectionOptions, getTimeSectionOptions } from './MultiSectionDigitalClock.utils'; -import { PickerOwnerState, PickerValidDate, TimeStepOptions, TimeView } from '../models'; +import { PickerOwnerState, PickerValidDate, TimeStepOptions } from '../models'; import { TimeViewWithMeridiem } from '../internals/models'; import { useControlledValueWithTimezone } from '../internals/hooks/useValueWithTimezone'; import { singleItemValueManager } from '../internals/utils/valueManagers'; @@ -180,10 +180,12 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi ); const isTimeDisabled = React.useCallback( - (rawValue: number, viewType: TimeView) => { + (rawValue: number, viewType: TimeViewWithMeridiem) => { const isAfter = createIsAfterIgnoreDatePart(disableIgnoringDatePartForTimeValidation, utils); const shouldCheckPastEnd = - viewType === 'hours' || (viewType === 'minutes' && views.includes('seconds')); + viewType === 'hours' || + (viewType === 'minutes' && views.includes('seconds')) || + viewType === 'meridiem'; const containsValidTime = ({ start, @@ -271,6 +273,15 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi return !containsValidTime({ start, end }) || !isValidValue(rawValue); } + case 'meridiem': { + const start = utils.setSeconds( + utils.setMinutes(utils.setHours(utils.startOfDay(valueOrReferenceDate), rawValue), 0), + 0, + ); + const end = utils.addSeconds(utils.addMinutes(utils.addHours(start, 11), 59), 59); + return !containsValidTime({ start, end }); + } + default: throw new Error('not supported'); } @@ -372,6 +383,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi isSelected: () => !!value && meridiemMode === 'am', isFocused: () => !!valueOrReferenceDate && meridiemMode === 'am', ariaLabel: amLabel, + isDisabled: () => isTimeDisabled(0, 'meridiem'), }, { value: 'pm', @@ -379,6 +391,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi isSelected: () => !!value && meridiemMode === 'pm', isFocused: () => !!valueOrReferenceDate && meridiemMode === 'pm', ariaLabel: pmLabel, + isDisabled: () => isTimeDisabled(12, 'meridiem'), }, ], }; diff --git a/packages/x-date-pickers/src/TimeClock/Clock.tsx b/packages/x-date-pickers/src/TimeClock/Clock.tsx index 7e569793d3e08..04cb211d0147e 100644 --- a/packages/x-date-pickers/src/TimeClock/Clock.tsx +++ b/packages/x-date-pickers/src/TimeClock/Clock.tsx @@ -19,13 +19,14 @@ import { formatMeridiem } from '../internals/utils/date-utils'; import { Meridiem } from '../internals/utils/time-utils'; import { FormProps } from '../internals/models/formProps'; import { usePickerPrivateContext } from '../internals/hooks/usePickerPrivateContext'; +import { TimeViewWithMeridiem } from '../internals/models'; export interface ClockProps extends ReturnType, FormProps { ampm: boolean; ampmInClock: boolean; autoFocus?: boolean; children: readonly React.ReactNode[]; - isTimeDisabled: (timeValue: number, type: TimeView) => boolean; + isTimeDisabled: (timeValue: number, type: TimeViewWithMeridiem) => boolean; minutesStep?: number; onChange: (value: number, isFinish?: PickerSelectionState) => void; /** @@ -413,7 +414,7 @@ export function Clock(inProps: ClockProps) { handleMeridiemChange('am')} - disabled={disabled || meridiemMode === null} + disabled={disabled || meridiemMode === null || isTimeDisabled(0, 'meridiem')} ownerState={ownerState} className={classes.amButton} title={formatMeridiem(utils, 'am')} @@ -423,7 +424,7 @@ export function Clock(inProps: ClockProps) { handleMeridiemChange('pm')} ownerState={ownerState} diff --git a/packages/x-date-pickers/src/TimeClock/TimeClock.tsx b/packages/x-date-pickers/src/TimeClock/TimeClock.tsx index c6d6bda63d048..2f3431c66c096 100644 --- a/packages/x-date-pickers/src/TimeClock/TimeClock.tsx +++ b/packages/x-date-pickers/src/TimeClock/TimeClock.tsx @@ -21,6 +21,7 @@ import { useControlledValueWithTimezone } from '../internals/hooks/useValueWithT import { singleItemValueManager } from '../internals/utils/valueManagers'; import { useClockReferenceDate } from '../internals/hooks/useClockReferenceDate'; import { usePickerPrivateContext } from '../internals/hooks/usePickerPrivateContext'; +import { TimeViewWithMeridiem } from '../internals/models'; const useUtilityClasses = (classes: Partial | undefined) => { const slots = { @@ -150,10 +151,12 @@ export const TimeClock = React.forwardRef(function TimeClock( ); const isTimeDisabled = React.useCallback( - (rawValue: number, viewType: TimeView) => { + (rawValue: number, viewType: TimeViewWithMeridiem) => { const isAfter = createIsAfterIgnoreDatePart(disableIgnoringDatePartForTimeValidation, utils); const shouldCheckPastEnd = - viewType === 'hours' || (viewType === 'minutes' && views.includes('seconds')); + viewType === 'hours' || + (viewType === 'minutes' && views.includes('seconds')) || + viewType === 'meridiem'; const containsValidTime = ({ start, @@ -239,6 +242,15 @@ export const TimeClock = React.forwardRef(function TimeClock( return !containsValidTime({ start, end }) || !isValidValue(rawValue); } + case 'meridiem': { + const start = utils.setSeconds( + utils.setMinutes(utils.setHours(utils.startOfDay(valueOrReferenceDate), rawValue), 0), + 0, + ); + const end = utils.addSeconds(utils.addMinutes(utils.addHours(start, 11), 59), 59); + return !containsValidTime({ start, end }); + } + default: throw new Error('not supported'); } diff --git a/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx b/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx index 3dce9ed968566..c2cf5c5365031 100644 --- a/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx +++ b/packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx @@ -9,7 +9,7 @@ import { PickersToolbarButton } from '../internals/components/PickersToolbarButt import { PickersToolbar } from '../internals/components/PickersToolbar'; import { arrayIncludes } from '../internals/utils/utils'; import { usePickerTranslations } from '../hooks/usePickerTranslations'; -import { useUtils } from '../internals/hooks/useUtils'; +import { useNow, useUtils } from '../internals/hooks/useUtils'; import { useMeridiemMode } from '../internals/hooks/date-helpers-hooks'; import { BaseToolbarProps, ExportedBaseToolbarProps } from '../internals/models/props/toolbar'; import { @@ -25,12 +25,24 @@ import { PickerToolbarOwnerState, useToolbarOwnerState, } from '../internals/hooks/useToolbarOwnerState'; +import { createIsAfterIgnoreDatePart } from '../internals/utils/time-utils'; +import { singleItemValueManager } from '../internals/utils/valueManagers'; +import { useClockReferenceDate } from '../internals/hooks/useClockReferenceDate'; +import { useControlledValueWithTimezone } from '../internals/hooks/useValueWithTimezone'; export interface TimePickerToolbarProps extends BaseToolbarProps, ExportedTimePickerToolbarProps { ampm?: boolean; ampmInClock?: boolean; + minTime?: PickerValidDate; + maxTime?: PickerValidDate; + disablePast?: boolean; + disableFuture?: boolean; + referenceDate?: PickerValidDate; + defaultValue?: PickerValidDate; + timezone?: string; + disableIgnoringDatePartForTimeValidation?: boolean; } export interface ExportedTimePickerToolbarProps extends ExportedBaseToolbarProps { @@ -157,11 +169,19 @@ function TimePickerToolbar(inProps: TimePickerToolbarProps) { const { ampm, ampmInClock, - value, + value: valueProp, isLandscape, onChange, className, classes: classesProp, + maxTime, + minTime, + disablePast, + disableFuture, + referenceDate: referenceDateProp, + defaultValue, + timezone: timezoneProp, + disableIgnoringDatePartForTimeValidation = false, ...other } = props; const utils = useUtils(); @@ -172,6 +192,49 @@ function TimePickerToolbar(inProps: TimePickerToolbarProps) { usePickerContext(); const showAmPmControl = Boolean(ampm && !ampmInClock && views.includes('hours')); + + const { value, timezone } = useControlledValueWithTimezone({ + name: 'TimePickerToolbar', + timezone: timezoneProp, + value: valueProp, + defaultValue, + referenceDate: referenceDateProp, + onChange, + valueManager: singleItemValueManager, + }); + const valueOrReferenceDate = useClockReferenceDate({ + value, + referenceDate: referenceDateProp, + utils, + props, + timezone, + }); + const now = useNow(timezone); + const isDisableTime = (time: number) => { + const isAfter = createIsAfterIgnoreDatePart(disableIgnoringDatePartForTimeValidation, utils); + const start = utils.setSeconds( + utils.setMinutes(utils.setHours(utils.startOfDay(valueOrReferenceDate), time), 0), + 0, + ); + const end = utils.addSeconds(utils.addMinutes(utils.addHours(start, 11), 59), 59); + if (minTime && isAfter(minTime, end)) { + return false; + } + + if (maxTime && isAfter(start, maxTime)) { + return false; + } + + if (disableFuture && isAfter(start, now)) { + return false; + } + + if (disablePast && isAfter(now, end)) { + return false; + } + + return true; + }; const { meridiemMode, handleMeridiemChange } = useMeridiemMode(value, ampm, onChange); const formatHours = (time: PickerValidDate) => @@ -241,7 +304,7 @@ function TimePickerToolbar(inProps: TimePickerToolbarProps) { typographyClassName={classes.ampmLabel} value={formatMeridiem(utils, 'am')} onClick={readOnly ? undefined : () => handleMeridiemChange('am')} - disabled={disabled} + disabled={disabled || !isDisableTime(0)} /> handleMeridiemChange('pm')} - disabled={disabled} + disabled={disabled || !isDisableTime(12)} /> )} @@ -271,13 +334,20 @@ TimePickerToolbar.propTypes = { */ classes: PropTypes.object, className: PropTypes.string, + defaultValue: PropTypes.object, + disableFuture: PropTypes.bool, + disableIgnoringDatePartForTimeValidation: PropTypes.bool, + disablePast: PropTypes.bool, /** * If `true`, show the toolbar even in desktop mode. * @default `true` for Desktop, `false` for Mobile. */ hidden: PropTypes.bool, isLandscape: PropTypes.bool.isRequired, + maxTime: PropTypes.object, + minTime: PropTypes.object, onChange: PropTypes.func.isRequired, + referenceDate: PropTypes.object, /** * The system prop that allows defining system overrides as well as additional CSS styles. */ @@ -286,6 +356,7 @@ TimePickerToolbar.propTypes = { PropTypes.func, PropTypes.object, ]), + timezone: PropTypes.string, titleId: PropTypes.string, /** * Toolbar date format. diff --git a/packages/x-date-pickers/src/TimePicker/shared.tsx b/packages/x-date-pickers/src/TimePicker/shared.tsx index 281caf40c139e..4da40c7a164f5 100644 --- a/packages/x-date-pickers/src/TimePicker/shared.tsx +++ b/packages/x-date-pickers/src/TimePicker/shared.tsx @@ -117,6 +117,15 @@ export function useTimePickerDefaultizedProps< toolbar: { ampm, ampmInClock: themeProps.ampmInClock, + minTime: themeProps.minTime, + maxTime: themeProps.maxTime, + disablePast: themeProps.disablePast, + disableFuture: themeProps.disableFuture, + disableIgnoringDatePartForTimeValidation: + themeProps.disableIgnoringDatePartForTimeValidation, + referenceDate: themeProps.referenceDate, + defaultValue: themeProps.defaultValue, + timezone: themeProps.timezone, ...themeProps.slotProps?.toolbar, }, },