Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[picker] Disable am/pm for maxTime/minTime, disableTime, and disableFuture props #15999

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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');
}
Expand Down Expand Up @@ -372,13 +383,15 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi
isSelected: () => !!value && meridiemMode === 'am',
isFocused: () => !!valueOrReferenceDate && meridiemMode === 'am',
ariaLabel: amLabel,
isDisabled: () => isTimeDisabled(0, 'meridiem'),
},
{
value: 'pm',
label: pmLabel,
isSelected: () => !!value && meridiemMode === 'pm',
isFocused: () => !!valueOrReferenceDate && meridiemMode === 'pm',
ariaLabel: pmLabel,
isDisabled: () => isTimeDisabled(12, 'meridiem'),
},
],
};
Expand Down
7 changes: 4 additions & 3 deletions packages/x-date-pickers/src/TimeClock/Clock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof useMeridiemMode>, 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;
/**
Expand Down Expand Up @@ -413,7 +414,7 @@ export function Clock(inProps: ClockProps) {
<ClockAmButton
data-testid="in-clock-am-btn"
onClick={readOnly ? undefined : () => handleMeridiemChange('am')}
disabled={disabled || meridiemMode === null}
disabled={disabled || meridiemMode === null || isTimeDisabled(0, 'meridiem')}
ownerState={ownerState}
className={classes.amButton}
title={formatMeridiem(utils, 'am')}
Expand All @@ -423,7 +424,7 @@ export function Clock(inProps: ClockProps) {
</ClockMeridiemText>
</ClockAmButton>
<ClockPmButton
disabled={disabled || meridiemMode === null}
disabled={disabled || meridiemMode === null || isTimeDisabled(12, 'meridiem')}
data-testid="in-clock-pm-btn"
onClick={readOnly ? undefined : () => handleMeridiemChange('pm')}
ownerState={ownerState}
Expand Down
16 changes: 14 additions & 2 deletions packages/x-date-pickers/src/TimeClock/TimeClock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TimeClockClasses> | undefined) => {
const slots = {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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');
}
Expand Down
79 changes: 75 additions & 4 deletions packages/x-date-pickers/src/TimePicker/TimePickerToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<PickerValue>,
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 {
Expand Down Expand Up @@ -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();
Expand All @@ -172,6 +192,49 @@ function TimePickerToolbar(inProps: TimePickerToolbarProps) {
usePickerContext<TimeViewWithMeridiem>();

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) =>
Expand Down Expand Up @@ -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)}
/>
<PickersToolbarButton
disableRipple
Expand All @@ -251,7 +314,7 @@ function TimePickerToolbar(inProps: TimePickerToolbarProps) {
typographyClassName={classes.ampmLabel}
value={formatMeridiem(utils, 'pm')}
onClick={readOnly ? undefined : () => handleMeridiemChange('pm')}
disabled={disabled}
disabled={disabled || !isDisableTime(12)}
/>
</TimePickerToolbarAmPmSelection>
)}
Expand All @@ -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.
*/
Expand All @@ -286,6 +356,7 @@ TimePickerToolbar.propTypes = {
PropTypes.func,
PropTypes.object,
]),
timezone: PropTypes.string,
titleId: PropTypes.string,
/**
* Toolbar date format.
Expand Down
9 changes: 9 additions & 0 deletions packages/x-date-pickers/src/TimePicker/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
Expand Down
Loading