Skip to content

Commit

Permalink
[pickers] Add referenceDate prop on TimeClock, DigitalClock and…
Browse files Browse the repository at this point in the history
… `MultiSectionDigitalClock` (#9356)
  • Loading branch information
flaviendelangle authored Jun 30, 2023
1 parent 3ee9e82 commit b53ee13
Show file tree
Hide file tree
Showing 29 changed files with 468 additions and 128 deletions.
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

0 comments on commit b53ee13

Please sign in to comment.