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

[pickers] Add referenceDate prop on TimeClock, DigitalClock and MultiSectionDigitalClock #9356

Merged
merged 11 commits into from
Jun 30, 2023
4 changes: 4 additions & 0 deletions docs/pages/x/api/date-pickers/digital-clock.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
"onViewChange": { "type": { "name": "func" } },
"openTo": { "type": { "name": "enum", "description": "'hours'" } },
"readOnly": { "type": { "name": "bool" } },
"referenceDate": {
"type": { "name": "any" },
"default": "The closest valid time using the validation props, except callbacks such as `shouldDisableTime`."
},
"shouldDisableClock": {
"type": { "name": "func" },
"deprecated": true,
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/x/api/date-pickers/month-calendar.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"readOnly": { "type": { "name": "bool" } },
"referenceDate": {
"type": { "name": "any" },
"default": "The closest valid month using the validation props, except callbacks such as `shouldDisableDate`."
"default": "The closest valid month using the validation props, except callbacks such as `shouldDisableMonth`."
},
"shouldDisableMonth": { "type": { "name": "func" } },
"sx": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
}
},
"readOnly": { "type": { "name": "bool" } },
"referenceDate": {
"type": { "name": "any" },
"default": "The closest valid time using the validation props, except callbacks such as `shouldDisableTime`."
},
"shouldDisableClock": {
"type": { "name": "func" },
"deprecated": true,
Expand Down
4 changes: 4 additions & 0 deletions docs/pages/x/api/date-pickers/time-clock.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
}
},
"readOnly": { "type": { "name": "bool" } },
"referenceDate": {
"type": { "name": "any" },
"default": "The closest valid time using the validation props, except callbacks such as `shouldDisableTime`."
},
"shouldDisableClock": {
"type": { "name": "func" },
"deprecated": true,
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/x/api/date-pickers/year-calendar.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"readOnly": { "type": { "name": "bool" } },
"referenceDate": {
"type": { "name": "any" },
"default": "The closest valid year using the validation props, except callbacks such as `shouldDisableDate`."
"default": "The closest valid year using the validation props, except callbacks such as `shouldDisableYear`."
},
"shouldDisableYear": { "type": { "name": "func" } },
"sx": {
Expand Down
1 change: 1 addition & 0 deletions docs/translations/api-docs/date-pickers/digital-clock.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"onViewChange": "Callback fired on view change.<br><br><strong>Signature:</strong><br><code>function(view: TView) =&gt; void</code><br><em>view:</em> The new view.",
"openTo": "The default visible view. Used when the component view is not controlled. Must be a valid option from <code>views</code> list.",
"readOnly": "If <code>true</code>, the picker views and text field are read-only.",
"referenceDate": "The date used to generate the new value when both <code>value</code> and <code>defaultValue</code> are empty.",
"shouldDisableClock": "Disable specific clock time.<br><br><strong>Signature:</strong><br><code>function(clockValue: number, view: TimeView) =&gt; boolean</code><br><em>clockValue:</em> The value to check.<br><em>view:</em> The clock type of the timeValue.<br> <em>returns</em> (boolean): If <code>true</code> the time will be disabled.",
"shouldDisableTime": "Disable specific time.<br><br><strong>Signature:</strong><br><code>function(value: TDate, view: TimeView) =&gt; boolean</code><br><em>value:</em> The value to check.<br><em>view:</em> The clock type of the timeValue.<br> <em>returns</em> (boolean): If <code>true</code> the time will be disabled.",
"skipDisabled": "If <code>true</code>, disabled digital clock items will not be rendered.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"onViewChange": "Callback fired on view change.<br><br><strong>Signature:</strong><br><code>function(view: TView) =&gt; void</code><br><em>view:</em> The new view.",
"openTo": "The default visible view. Used when the component view is not controlled. Must be a valid option from <code>views</code> list.",
"readOnly": "If <code>true</code>, the picker views and text field are read-only.",
"referenceDate": "The date used to generate the new value when both <code>value</code> and <code>defaultValue</code> are empty.",
"shouldDisableClock": "Disable specific clock time.<br><br><strong>Signature:</strong><br><code>function(clockValue: number, view: TimeView) =&gt; boolean</code><br><em>clockValue:</em> The value to check.<br><em>view:</em> The clock type of the timeValue.<br> <em>returns</em> (boolean): If <code>true</code> the time will be disabled.",
"shouldDisableTime": "Disable specific time.<br><br><strong>Signature:</strong><br><code>function(value: TDate, view: TimeView) =&gt; boolean</code><br><em>value:</em> The value to check.<br><em>view:</em> The clock type of the timeValue.<br> <em>returns</em> (boolean): If <code>true</code> the time will be disabled.",
"skipDisabled": "If <code>true</code>, disabled digital clock items will not be rendered.",
Expand Down
1 change: 1 addition & 0 deletions docs/translations/api-docs/date-pickers/time-clock.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"onViewChange": "Callback fired on view change.<br><br><strong>Signature:</strong><br><code>function(view: TView) =&gt; void</code><br><em>view:</em> The new view.",
"openTo": "The default visible view. Used when the component view is not controlled. Must be a valid option from <code>views</code> list.",
"readOnly": "If <code>true</code>, the picker views and text field are read-only.",
"referenceDate": "The date used to generate the new value when both <code>value</code> and <code>defaultValue</code> are empty.",
"shouldDisableClock": "Disable specific clock time.<br><br><strong>Signature:</strong><br><code>function(clockValue: number, view: TimeView) =&gt; boolean</code><br><em>clockValue:</em> The value to check.<br><em>view:</em> The clock type of the timeValue.<br> <em>returns</em> (boolean): If <code>true</code> the time will be disabled.",
"shouldDisableTime": "Disable specific time.<br><br><strong>Signature:</strong><br><code>function(value: TDate, view: TimeView) =&gt; boolean</code><br><em>value:</em> The value to check.<br><em>view:</em> The clock type of the timeValue.<br> <em>returns</em> (boolean): If <code>true</code> the time will be disabled.",
"slotProps": "The props used for each component slot.",
Expand Down
1 change: 0 additions & 1 deletion packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,6 @@ export class AdapterDayjs implements MuiPickersAdapter<Dayjs, string> {
}

let parsedValue: Dayjs;

if (timezone === 'UTC') {
parsedValue = this.createUTCDate(value);
} else if (timezone === 'system' || (timezone === 'default' && !this.hasTimezonePlugin())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ describe('<DateCalendar />', () => {

userEvent.mousePress(screen.getByRole('gridcell', { name: '2' }));
expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2018, 0, 2));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2018, 0, 2));
});

it('should use `referenceDate` when no value defined', () => {
Expand All @@ -190,7 +190,7 @@ describe('<DateCalendar />', () => {

userEvent.mousePress(screen.getByRole('gridcell', { name: '2' }));
expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2018, 0, 2, 12, 30));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2018, 0, 2, 12, 30));
});

it('should not use `referenceDate` when a value is defined', () => {
Expand All @@ -207,7 +207,7 @@ describe('<DateCalendar />', () => {

userEvent.mousePress(screen.getByRole('gridcell', { name: '2' }));
expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 0, 2, 12, 20));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 0, 2, 12, 20));
});

it('should not use `referenceDate` when a defaultValue is defined', () => {
Expand All @@ -224,7 +224,7 @@ describe('<DateCalendar />', () => {

userEvent.mousePress(screen.getByRole('gridcell', { name: '2' }));
expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 0, 2, 12, 20));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 0, 2, 12, 20));
});

it('should keep the time of the currently provided date', () => {
Expand All @@ -241,7 +241,7 @@ describe('<DateCalendar />', () => {

userEvent.mousePress(screen.getByRole('gridcell', { name: '2' }));
expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(
expect(onChange.lastCall.firstArg).toEqualDateTime(
adapterToUse.date(new Date(2018, 0, 2, 11, 11, 11)),
);
});
Expand Down Expand Up @@ -304,7 +304,7 @@ describe('<DateCalendar />', () => {
fireEvent.click(april);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 3, 6));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 3, 6));
});

it('should respect minDate when selecting closest enabled date', () => {
Expand All @@ -324,7 +324,7 @@ describe('<DateCalendar />', () => {
fireEvent.click(april);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 3, 7));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 3, 7));
});

it('should respect maxDate when selecting closest enabled date', () => {
Expand All @@ -344,7 +344,7 @@ describe('<DateCalendar />', () => {
fireEvent.click(april);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 3, 22));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 3, 22));
});

it('should go to next view without changing the date when no date of the new month is enabled', () => {
Expand Down Expand Up @@ -384,7 +384,7 @@ describe('<DateCalendar />', () => {
fireEvent.click(april);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2018, 3, 1, 12, 30));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2018, 3, 1, 12, 30));
});

it('should not use `referenceDate` when a value is defined', () => {
Expand All @@ -404,7 +404,7 @@ describe('<DateCalendar />', () => {
fireEvent.click(april);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 3, 1, 12, 20));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 3, 1, 12, 20));
});

it('should not use `referenceDate` when a defaultValue is defined', () => {
Expand All @@ -424,7 +424,7 @@ describe('<DateCalendar />', () => {
fireEvent.click(april);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 3, 1, 12, 20));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 3, 1, 12, 20));
});
});

Expand Down Expand Up @@ -454,7 +454,7 @@ describe('<DateCalendar />', () => {
fireEvent.click(year2022);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2022, 4, 1));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 4, 1));
});

it('should respect minDate when selecting closest enabled date', () => {
Expand All @@ -474,7 +474,7 @@ describe('<DateCalendar />', () => {
fireEvent.click(year2017);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2017, 4, 12));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2017, 4, 12));
});

it('should respect maxDate when selecting closest enabled date', () => {
Expand All @@ -494,7 +494,7 @@ describe('<DateCalendar />', () => {
fireEvent.click(year2022);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2022, 2, 31));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 2, 31));
});

it('should go to next view without changing the date when no date of the new year is enabled', () => {
Expand Down Expand Up @@ -559,7 +559,7 @@ describe('<DateCalendar />', () => {
fireEvent.click(year2022);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2022, 0, 1, 12, 30));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 0, 1, 12, 30));
});

it('should not use `referenceDate` when a value is defined', () => {
Expand All @@ -579,7 +579,7 @@ describe('<DateCalendar />', () => {
fireEvent.click(year2022);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2022, 0, 1, 12, 20));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 0, 1, 12, 20));
});

it('should not use `referenceDate` when a defaultValue is defined', () => {
Expand All @@ -599,7 +599,7 @@ describe('<DateCalendar />', () => {
fireEvent.click(year2022);

expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2022, 0, 1, 12, 20));
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 0, 1, 12, 20));
});
});

Expand Down
30 changes: 20 additions & 10 deletions packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TimeView } from '../models';
import { DIGITAL_CLOCK_VIEW_HEIGHT } from '../internals/constants/dimensions';
import { useControlledValueWithTimezone } from '../internals/hooks/useValueWithTimezone';
import { singleItemValueManager } from '../internals/utils/valueManagers';
import { useClockReferenceDate } from '../internals/hooks/useClockReferenceDate';

const useUtilityClasses = (ownerState: DigitalClockProps<any>) => {
const { classes } = ownerState;
Expand Down Expand Up @@ -104,6 +105,8 @@ export const DigitalClock = React.forwardRef(function DigitalClock<TDate extends
slots,
slotProps,
value: valueProp,
defaultValue,
referenceDate: referenceDateProp,
disableIgnoringDatePartForTimeValidation = false,
maxTime,
minTime,
Expand All @@ -113,7 +116,6 @@ export const DigitalClock = React.forwardRef(function DigitalClock<TDate extends
shouldDisableClock,
shouldDisableTime,
onChange,
defaultValue,
view: inView,
openTo,
onViewChange,
Expand Down Expand Up @@ -154,6 +156,14 @@ export const DigitalClock = React.forwardRef(function DigitalClock<TDate extends
const ClockItem = slots?.digitalClockItem ?? components?.DigitalClockItem ?? DigitalClockItem;
const clockItemProps = slotProps?.digitalClockItem ?? componentsProps?.digitalClockItem;

const valueOrReferenceDate = useClockReferenceDate({
value,
referenceDate: referenceDateProp,
utils,
props,
timezone,
});

const handleValueChange = useEventCallback((newValue: TDate | null) =>
handleRawValueChange(newValue, 'finish'),
);
Expand Down Expand Up @@ -189,11 +199,6 @@ export const DigitalClock = React.forwardRef(function DigitalClock<TDate extends
containerRef.current.scrollTop = offsetTop - 4;
});

const selectedTimeOrMidnight = React.useMemo(
() => value || utils.setSeconds(utils.setMinutes(utils.setHours(now, 0), 0), 0),
[value, now, utils],
);

const isTimeDisabled = React.useCallback(
(valueToCheck: TDate) => {
const isAfter = createIsAfterIgnoreDatePart(disableIgnoringDatePartForTimeValidation, utils);
Expand Down Expand Up @@ -251,15 +256,15 @@ export const DigitalClock = React.forwardRef(function DigitalClock<TDate extends
);

const timeOptions = React.useMemo(() => {
const startOfDay = utils.startOfDay(selectedTimeOrMidnight);
const startOfDay = utils.startOfDay(valueOrReferenceDate);
return [
startOfDay,
...Array.from({ length: Math.ceil((24 * 60) / timeStep) - 1 }, (_, index) =>
utils.addMinutes(startOfDay, timeStep * (index + 1)),
),
utils.endOfDay(selectedTimeOrMidnight),
utils.endOfDay(valueOrReferenceDate),
];
}, [selectedTimeOrMidnight, timeStep, utils]);
}, [valueOrReferenceDate, timeStep, utils]);

return (
<DigitalClockRoot
Expand Down Expand Up @@ -380,7 +385,7 @@ DigitalClock.propTypes = {
minutesStep: PropTypes.number,
/**
* Callback fired when the value changes.
* @template TDate
* @template TDate, TView
* @param {TDate | null} value The new value.
* @param {PickerSelectionState | undefined} selectionState Indicates if the date selection is complete.
* @param {TView | undefined} selectedView Indicates the view in which the selection has been made.
Expand Down Expand Up @@ -410,6 +415,11 @@ DigitalClock.propTypes = {
* @default false
*/
readOnly: PropTypes.bool,
/**
* The date used to generate the new value when both `value` and `defaultValue` are empty.
* @default The closest valid time using the validation props, except callbacks such as `shouldDisableTime`.
*/
referenceDate: PropTypes.any,
/**
* Disable specific clock time.
* @param {number} clockValue The value to check.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { DigitalClock } from '@mui/x-date-pickers/DigitalClock';
import { adapterToUse, createPickerRenderer } from 'test/utils/pickers-utils';
import { digitalClockHandler } from 'test/utils/pickers/viewHandlers';

describe('<DigitalClock />', () => {
const { render } = createPickerRenderer();

describe('Reference date', () => {
it('should use `referenceDate` when no value defined', () => {
const onChange = spy();

render(
<DigitalClock
onChange={onChange}
referenceDate={adapterToUse.date(new Date(2018, 0, 1, 12, 30))}
/>,
);

digitalClockHandler.setViewValue(
adapterToUse,
adapterToUse.setMinutes(adapterToUse.setHours(adapterToUse.date(), 15), 30),
);
expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2018, 0, 1, 15, 30));
});

it('should not use `referenceDate` when a value is defined', () => {
const onChange = spy();

render(
<DigitalClock
onChange={onChange}
value={adapterToUse.date(new Date(2019, 0, 1, 12, 30))}
referenceDate={adapterToUse.date(new Date(2018, 0, 1, 15, 30))}
/>,
);

digitalClockHandler.setViewValue(
adapterToUse,
adapterToUse.setMinutes(adapterToUse.setHours(adapterToUse.date(), 15), 30),
);
expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 0, 1, 15, 30));
});

it('should not use `referenceDate` when a defaultValue is defined', () => {
const onChange = spy();

render(
<DigitalClock
onChange={onChange}
defaultValue={adapterToUse.date(new Date(2019, 0, 1, 12, 30))}
referenceDate={adapterToUse.date(new Date(2018, 0, 1, 15, 30))}
/>,
);

digitalClockHandler.setViewValue(
adapterToUse,
adapterToUse.setMinutes(adapterToUse.setHours(adapterToUse.date(), 15), 30),
);
expect(onChange.callCount).to.equal(1);
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 0, 1, 15, 30));
});
});
});
Loading