From 1e6d35246efd6d35595566b0ea0937ccfa4b0eb2 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 26 Feb 2024 16:09:15 +0200 Subject: [PATCH 01/18] Adapt `dayViewRangeValidation` for DesktopDTR with single calendar --- .../testDayViewRangeValidation.tsx | 70 ++++++++++++------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/test/utils/pickers/describeRangeValidation/testDayViewRangeValidation.tsx b/test/utils/pickers/describeRangeValidation/testDayViewRangeValidation.tsx index 6d5e8ead8f970..78c0c987506ca 100644 --- a/test/utils/pickers/describeRangeValidation/testDayViewRangeValidation.tsx +++ b/test/utils/pickers/describeRangeValidation/testDayViewRangeValidation.tsx @@ -5,15 +5,10 @@ import { adapterToUse } from 'test/utils/pickers'; const isDisable = (el: HTMLElement) => el.getAttribute('disabled') !== null; -const isFieldElement = (el: HTMLElement) => el.className.includes('MuiPickersInput'); - -const testDisabledDate = (day: string, expectedAnswer: boolean[], isDesktop: boolean) => { - expect( - screen - .getAllByText(day) - .filter((el) => !isFieldElement(el)) - .map(isDisable), - ).to.deep.equal(isDesktop ? expectedAnswer : expectedAnswer.slice(0, 1)); +const testDisabledDate = (day: string, expectedAnswer: boolean[], isSingleCalendar: boolean) => { + expect(screen.getAllByRole('gridcell', { name: day }).map(isDisable)).to.deep.equal( + isSingleCalendar ? expectedAnswer.slice(0, 1) : expectedAnswer, + ); }; const testMonthSwitcherAreDisable = (areDisable: [boolean, boolean]) => { @@ -44,6 +39,7 @@ export function testDayViewRangeValidation(ElementToTest, getOptions) { } const isDesktop = variant === 'desktop'; + const includesTimeView = views.includes('hours'); const defaultProps = { referenceDate: adapterToUse.date('2018-03-12'), @@ -62,8 +58,8 @@ export function testDayViewRangeValidation(ElementToTest, getOptions) { />, ); - testDisabledDate('10', [false, true], isDesktop); - testDisabledDate('11', [true, true], isDesktop); + testDisabledDate('10', [false, true], !isDesktop || includesTimeView); + testDisabledDate('11', [true, true], !isDesktop || includesTimeView); }); it('should apply disablePast', function test() { @@ -80,14 +76,26 @@ export function testDayViewRangeValidation(ElementToTest, getOptions) { const tomorrow = adapterToUse.addDays(now, 1); const yesterday = adapterToUse.addDays(now, -1); - testDisabledDate(adapterToUse.format(now, 'dayOfMonth'), [false, false], isDesktop); - testDisabledDate(adapterToUse.format(tomorrow, 'dayOfMonth'), [false, false], isDesktop); + testDisabledDate( + adapterToUse.format(now, 'dayOfMonth'), + [false, false], + !isDesktop || includesTimeView, + ); + testDisabledDate( + adapterToUse.format(tomorrow, 'dayOfMonth'), + [false, false], + !isDesktop || includesTimeView, + ); if (!adapterToUse.isSameMonth(yesterday, tomorrow)) { setProps({ value: [yesterday, null] }); clock.runToLast(); } - testDisabledDate(adapterToUse.format(yesterday, 'dayOfMonth'), [true, false], isDesktop); + testDisabledDate( + adapterToUse.format(yesterday, 'dayOfMonth'), + [true, false], + !isDesktop || includesTimeView, + ); }); it('should apply disableFuture', function test() { @@ -104,14 +112,26 @@ export function testDayViewRangeValidation(ElementToTest, getOptions) { const tomorrow = adapterToUse.addDays(now, 1); const yesterday = adapterToUse.addDays(now, -1); - testDisabledDate(adapterToUse.format(now, 'dayOfMonth'), [false, true], isDesktop); - testDisabledDate(adapterToUse.format(tomorrow, 'dayOfMonth'), [true, true], isDesktop); + testDisabledDate( + adapterToUse.format(now, 'dayOfMonth'), + [false, true], + !isDesktop || includesTimeView, + ); + testDisabledDate( + adapterToUse.format(tomorrow, 'dayOfMonth'), + [true, true], + !isDesktop || includesTimeView, + ); if (!adapterToUse.isSameMonth(yesterday, tomorrow)) { setProps({ value: [yesterday, null] }); clock.runToLast(); } - testDisabledDate(adapterToUse.format(yesterday, 'dayOfMonth'), [false, true], isDesktop); + testDisabledDate( + adapterToUse.format(yesterday, 'dayOfMonth'), + [false, true], + !isDesktop || includesTimeView, + ); }); it('should apply minDate', function test() { @@ -125,10 +145,10 @@ export function testDayViewRangeValidation(ElementToTest, getOptions) { />, ); - testDisabledDate('1', [true, false], isDesktop); - testDisabledDate('3', [true, false], isDesktop); - testDisabledDate('4', [false, false], isDesktop); - testDisabledDate('15', [false, false], isDesktop); + testDisabledDate('1', [true, false], !isDesktop || includesTimeView); + testDisabledDate('3', [true, false], !isDesktop || includesTimeView); + testDisabledDate('4', [false, false], !isDesktop || includesTimeView); + testDisabledDate('15', [false, false], !isDesktop || includesTimeView); testMonthSwitcherAreDisable([true, false]); }); @@ -144,10 +164,10 @@ export function testDayViewRangeValidation(ElementToTest, getOptions) { />, ); - testDisabledDate('1', [false, true], isDesktop); - testDisabledDate('4', [false, true], isDesktop); - testDisabledDate('5', [true, true], isDesktop); - testDisabledDate('15', [true, true], isDesktop); + testDisabledDate('1', [false, true], !isDesktop || includesTimeView); + testDisabledDate('4', [false, true], !isDesktop || includesTimeView); + testDisabledDate('5', [true, true], !isDesktop || includesTimeView); + testDisabledDate('15', [true, true], !isDesktop || includesTimeView); testMonthSwitcherAreDisable([false, true]); }); From 5c2e109a39637f76a59c5fc4154ff42d93b6b567 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 27 Feb 2024 12:08:50 +0200 Subject: [PATCH 02/18] Fix validation props behavior --- .../src/DateTimeRangePicker/shared.tsx | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/x-date-pickers-pro/src/DateTimeRangePicker/shared.tsx b/packages/x-date-pickers-pro/src/DateTimeRangePicker/shared.tsx index cba6a59a1aa25..cbbaa6102347b 100644 --- a/packages/x-date-pickers-pro/src/DateTimeRangePicker/shared.tsx +++ b/packages/x-date-pickers-pro/src/DateTimeRangePicker/shared.tsx @@ -170,8 +170,27 @@ export function useDateTimeRangePickerDefaultizedProps< ampm, disableFuture: themeProps.disableFuture ?? false, disablePast: themeProps.disablePast ?? false, - minDate: applyDefaultDate(utils, themeProps.minDate, defaultDates.minDate), - maxDate: applyDefaultDate(utils, themeProps.maxDate, defaultDates.maxDate), + minDate: applyDefaultDate( + utils, + themeProps.minDateTime ?? themeProps.minDate, + defaultDates.minDate, + ), + maxDate: applyDefaultDate( + utils, + themeProps.maxDateTime ?? themeProps.maxDate, + defaultDates.maxDate, + ), + minTime: themeProps.minDateTime ?? themeProps.minTime, + maxTime: themeProps.maxDateTime ?? themeProps.maxTime, + disableIgnoringDatePartForTimeValidation: + themeProps.disableIgnoringDatePartForTimeValidation ?? + Boolean( + themeProps.minDateTime || + themeProps.maxDateTime || + // allow digital clocks to correctly check time validity: https://github.com/mui/mui-x/issues/12048 + themeProps.disablePast || + themeProps.disableFuture, + ), slots: { tabs: DateTimeRangePickerTabs, toolbar: DateTimeRangePickerToolbar, From 33f73e4e2186ae1fd9f45858b418a0da6a00baa5 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 27 Feb 2024 15:14:27 +0200 Subject: [PATCH 03/18] Add `DesktopDateTimeRangePicker` describe tests --- ...cribes.DesktopDateTimeRangePicker.test.tsx | 157 ++++++++++++++++++ .../testControlledUnControlled.tsx | 27 ++- .../describeValue/testPickerActionBar.tsx | 6 +- .../testPickerOpenCloseLifeCycle.tsx | 26 +-- test/utils/pickers/misc.ts | 6 + test/utils/pickers/openPicker.ts | 7 +- 6 files changed, 198 insertions(+), 31 deletions(-) create mode 100644 packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/describes.DesktopDateTimeRangePicker.test.tsx diff --git a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/describes.DesktopDateTimeRangePicker.test.tsx b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/describes.DesktopDateTimeRangePicker.test.tsx new file mode 100644 index 0000000000000..9c29cbf35fcf0 --- /dev/null +++ b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/describes.DesktopDateTimeRangePicker.test.tsx @@ -0,0 +1,157 @@ +import * as React from 'react'; +import { describeConformance, screen, userEvent } from '@mui-internal/test-utils'; +import { + createPickerRenderer, + adapterToUse, + expectFieldValueV7, + describeValue, + describePicker, + describeRangeValidation, + wrapPickerMount, + getFieldSectionsContainer, +} from 'test/utils/pickers'; +import { DesktopDateTimeRangePicker } from '../DesktopDateTimeRangePicker'; + +describe(' - Describes', () => { + const { render, clock } = createPickerRenderer({ + clock: 'fake', + }); + + describePicker(DesktopDateTimeRangePicker, { + render, + fieldType: 'multi-input', + variant: 'desktop', + }); + + describeRangeValidation(DesktopDateTimeRangePicker, () => ({ + render, + clock, + views: ['day', 'hours', 'minutes'], + componentFamily: 'picker', + variant: 'desktop', + })); + + describeConformance(, () => ({ + classes: {} as any, + render, + muiName: 'MuiDesktopDateTimeRangePicker', + wrapMount: wrapPickerMount, + refInstanceof: window.HTMLDivElement, + skip: [ + 'componentProp', + 'componentsProp', + 'themeDefaultProps', + 'themeStyleOverrides', + 'themeVariants', + 'mergeClassName', + 'propsSpread', + 'rootClass', + 'reactTestRenderer', + ], + })); + + describeValue(DesktopDateTimeRangePicker, () => ({ + render, + componentFamily: 'picker', + type: 'date-time-range', + variant: 'desktop', + initialFocus: 'start', + clock, + values: [ + // initial start and end dates + [adapterToUse.date('2018-01-01T11:30:00'), adapterToUse.date('2018-01-04T11:45:00')], + // start and end dates after `setNewValue` + [adapterToUse.date('2018-01-02T12:35:00'), adapterToUse.date('2018-01-05T12:50:00')], + ], + emptyValue: [null, null], + assertRenderedValue: (expectedValues: any[]) => { + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + const expectedPlaceholder = hasMeridiem ? 'MM/DD/YYYY hh:mm aa' : 'MM/DD/YYYY hh:mm'; + + const startSectionsContainer = getFieldSectionsContainer(0); + const expectedStartValueStr = expectedValues[0] + ? adapterToUse.format( + expectedValues[0], + hasMeridiem ? 'keyboardDateTime12h' : 'keyboardDateTime24h', + ) + : expectedPlaceholder; + expectFieldValueV7(startSectionsContainer, expectedStartValueStr); + + const endSectionsContainer = getFieldSectionsContainer(1); + const expectedEndValueStr = expectedValues[1] + ? adapterToUse.format( + expectedValues[1], + hasMeridiem ? 'keyboardDateTime12h' : 'keyboardDateTime24h', + ) + : expectedPlaceholder; + expectFieldValueV7(endSectionsContainer, expectedEndValueStr); + }, + setNewValue: ( + value, + { isOpened, applySameValue, setEndDate = false, selectSection, pressKey }, + ) => { + let newValue: any[]; + if (applySameValue) { + newValue = value; + } else if (setEndDate) { + newValue = [ + value[0], + adapterToUse.addMinutes(adapterToUse.addHours(adapterToUse.addDays(value[1], 1), 1), 5), + ]; + } else { + newValue = [ + adapterToUse.addMinutes(adapterToUse.addHours(adapterToUse.addDays(value[0], 1), 1), 5), + value[1], + ]; + } + if (isOpened) { + userEvent.mousePress( + screen.getByRole('gridcell', { + name: adapterToUse.getDate(newValue[setEndDate ? 1 : 0]).toString(), + }), + ); + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + const hours = adapterToUse.format( + newValue[setEndDate ? 1 : 0], + hasMeridiem ? 'hours12h' : 'hours24h', + ); + const hoursNumber = adapterToUse.getHours(newValue[setEndDate ? 1 : 0]); + userEvent.mousePress(screen.getByRole('option', { name: `${parseInt(hours, 10)} hours` })); + userEvent.mousePress( + screen.getByRole('option', { + name: `${adapterToUse.getMinutes(newValue[setEndDate ? 1 : 0])} minutes`, + }), + ); + if (hasMeridiem) { + // meridiem is an extra view on `DesktopDateTimeRangePicker` + // we need to click it to finish selection + userEvent.mousePress( + screen.getByRole('option', { name: hoursNumber >= 12 ? 'PM' : 'AM' }), + ); + } + } else { + selectSection('day'); + pressKey(undefined, 'ArrowUp'); + + selectSection('hours'); + pressKey(undefined, 'ArrowUp'); + + selectSection('minutes'); + pressKey(undefined, 'PageUp'); // increment by 5 minutes + + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + if (hasMeridiem) { + selectSection('meridiem'); + const previousHours = adapterToUse.getHours(value[setEndDate ? 1 : 0]); + const newHours = adapterToUse.getHours(newValue[setEndDate ? 1 : 0]); + // update meridiem section if it changed + if ((previousHours < 12 && newHours >= 12) || (previousHours >= 12 && newHours < 12)) { + pressKey(undefined, 'ArrowUp'); + } + } + } + + return newValue; + }, + })); +}); diff --git a/test/utils/pickers/describeValue/testControlledUnControlled.tsx b/test/utils/pickers/describeValue/testControlledUnControlled.tsx index 63477036c9b61..b173f3e0e0ee2 100644 --- a/test/utils/pickers/describeValue/testControlledUnControlled.tsx +++ b/test/utils/pickers/describeValue/testControlledUnControlled.tsx @@ -28,6 +28,9 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( const params = pickerParams as DescribeValueOptions<'picker', any>; + const isRangeType = ['date-range', 'date-time-range'].includes(params.type); + const isDesktopRange = params.variant === 'desktop' && isRangeType; + describe('Controlled / uncontrolled value', () => { it('should render `props.defaultValue` if no `props.value` is passed', () => { renderWithProps({ enableAccessibleFieldDOMStructure: true, defaultValue: values[0] }); @@ -170,10 +173,7 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( }); it('should have correct labelledby relationship when toolbar is shown', () => { - if ( - componentFamily !== 'picker' || - (params.variant === 'desktop' && params.type === 'date-range') - ) { + if (componentFamily !== 'picker' || isDesktopRange) { return; } @@ -188,10 +188,7 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( }); it('should have correct labelledby relationship with provided label when toolbar is hidden', () => { - if ( - componentFamily !== 'picker' || - (params.variant === 'desktop' && params.type === 'date-range') - ) { + if (componentFamily !== 'picker' || isDesktopRange) { return; } @@ -199,7 +196,7 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( enableAccessibleFieldDOMStructure: true, open: true, slotProps: { toolbar: { hidden: true } }, - ...(params.type === 'date-range' + ...(isRangeType ? { localeText: { start: 'test', @@ -213,10 +210,7 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( }); it('should have correct labelledby relationship without label and hidden toolbar but external props', () => { - if ( - componentFamily !== 'picker' || - (params.variant === 'desktop' && params.type === 'date-range') - ) { + if (componentFamily !== 'picker' || isDesktopRange) { return; } @@ -226,7 +220,7 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( = ( expect(fieldRoot).to.have.class(inputBaseClasses.error); expect(fieldRoot).to.have.attribute('aria-invalid', 'true'); - if (params.type === 'date-range' && !params.isSingleInput) { + if ( + (params.type === 'date-range' || params.type === 'date-time-range') && + !params.isSingleInput + ) { const fieldRootEnd = getFieldInputRoot(1); expect(fieldRootEnd).to.have.class(inputBaseClasses.error); expect(fieldRootEnd).to.have.attribute('aria-invalid', 'true'); diff --git a/test/utils/pickers/describeValue/testPickerActionBar.tsx b/test/utils/pickers/describeValue/testPickerActionBar.tsx index db94681f65c82..4070b908da148 100644 --- a/test/utils/pickers/describeValue/testPickerActionBar.tsx +++ b/test/utils/pickers/describeValue/testPickerActionBar.tsx @@ -27,6 +27,8 @@ export const testPickerActionBar: DescribeValueTestSuite = ( return; } + const isRangeType = ['date-range', 'date-time-range'].includes(pickerParams.type); + describe('Picker action bar', () => { describe('clear action', () => { it('should call onClose, onChange with empty value and onAccept with empty value', () => { @@ -105,7 +107,7 @@ export const testPickerActionBar: DescribeValueTestSuite = ( expect(onChange.callCount).to.equal( getExpectedOnChangeCount(componentFamily, pickerParams) + 1, ); - if (pickerParams.type === 'date-range') { + if (isRangeType) { values[0].forEach((value, index) => { expect(onChange.lastCall.args[0][index]).toEqualDateTime(value); }); @@ -245,7 +247,7 @@ export const testPickerActionBar: DescribeValueTestSuite = ( let startOfToday: any; if (pickerParams.type === 'date') { startOfToday = adapterToUse.startOfDay(adapterToUse.date()); - } else if (pickerParams.type === 'date-range') { + } else if (isRangeType) { startOfToday = [adapterToUse.date(), adapterToUse.date()]; } else { startOfToday = adapterToUse.date(); diff --git a/test/utils/pickers/describeValue/testPickerOpenCloseLifeCycle.tsx b/test/utils/pickers/describeValue/testPickerOpenCloseLifeCycle.tsx index fc0824b139e4d..e376828605274 100644 --- a/test/utils/pickers/describeValue/testPickerOpenCloseLifeCycle.tsx +++ b/test/utils/pickers/describeValue/testPickerOpenCloseLifeCycle.tsx @@ -16,8 +16,8 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite return; } - const viewWrapperRole = - pickerParams.type === 'date-range' && pickerParams.variant === 'desktop' ? 'tooltip' : 'dialog'; + const isRangeType = ['date-range', 'date-time-range'].includes(pickerParams.type); + const viewWrapperRole = isRangeType && pickerParams.variant === 'desktop' ? 'tooltip' : 'dialog'; describe('Picker open / close lifecycle', () => { it('should not open on mount if `props.open` is false', () => { @@ -70,7 +70,7 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite // Change the value let newValue = setNewValue(values[0], { isOpened: true, selectSection, pressKey }); expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily, pickerParams)); - if (pickerParams.type === 'date-range') { + if (isRangeType) { newValue = setNewValue(newValue, { isOpened: true, setEndDate: true, @@ -128,7 +128,7 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite // Change the value let newValue = setNewValue(values[0], { isOpened: true, selectSection, pressKey }); expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily, pickerParams)); - if (pickerParams.type === 'date-range') { + if (isRangeType) { newValue = setNewValue(newValue, { isOpened: true, setEndDate: true, @@ -165,7 +165,7 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite // Change the value (same value) setNewValue(values[0], { isOpened: true, applySameValue: true, selectSection, pressKey }); - if (pickerParams.type === 'date-range') { + if (isRangeType) { setNewValue(values[0], { isOpened: true, applySameValue: true, @@ -202,7 +202,7 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite let newValue = setNewValue(values[0], { isOpened: true, selectSection, pressKey }); const initialChangeCount = getExpectedOnChangeCount(componentFamily, pickerParams); expect(onChange.callCount).to.equal(initialChangeCount); - if (pickerParams.type === 'date-range') { + if (isRangeType) { newValue = setNewValue(newValue, { isOpened: true, setEndDate: true, @@ -220,8 +220,12 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite // Change the value let newValueBis = setNewValue(newValue, { isOpened: true, selectSection, pressKey }); - if (pickerParams.type === 'date-range') { - expect(onChange.callCount).to.equal(3); + if (isRangeType) { + expect(onChange.callCount).to.equal( + initialChangeCount + + getExpectedOnChangeCount(componentFamily, pickerParams) * 2 - + (pickerParams.type === 'date-time-range' ? 1 : 0), + ); newValueBis = setNewValue(newValueBis, { isOpened: true, setEndDate: true, @@ -269,7 +273,7 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite userEvent.keyPress(document.activeElement!, { key: 'Escape' }); expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily, pickerParams)); expect(onAccept.callCount).to.equal(1); - if (pickerParams.type === 'date-range') { + if (isRangeType) { newValue.forEach((value, index) => { expect(onChange.lastCall.args[0][index]).toEqualDateTime(value); }); @@ -281,7 +285,7 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite it('should call onClose when clicking outside of the picker without prior change', function test() { // TODO: Fix this test and enable it on mobile and date-range - if (pickerParams.variant === 'mobile' || pickerParams.type === 'date-range') { + if (pickerParams.variant === 'mobile' || isRangeType) { this.skip(); } @@ -310,7 +314,7 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite it('should call onClose and onAccept with the live value when clicking outside of the picker', function test() { // TODO: Fix this test and enable it on mobile and date-range - if (pickerParams.variant === 'mobile' || pickerParams.type === 'date-range') { + if (pickerParams.variant === 'mobile' || isRangeType) { this.skip(); } diff --git a/test/utils/pickers/misc.ts b/test/utils/pickers/misc.ts index 14afbe8208742..9e189dc2c8dc8 100644 --- a/test/utils/pickers/misc.ts +++ b/test/utils/pickers/misc.ts @@ -40,6 +40,12 @@ export const getExpectedOnChangeCount = ( params.variant === 'desktop' ? 'multi-section-digital-clock' : 'clock', ); } + if (componentFamily === 'picker' && params.type === 'date-time-range') { + return ( + getChangeCountForComponentFamily(componentFamily) + + getChangeCountForComponentFamily('multi-section-digital-clock') + ); + } if (componentFamily === 'clock') { // the `TimeClock` fires change for both touch move and touch end // but does not have meridiem control diff --git a/test/utils/pickers/openPicker.ts b/test/utils/pickers/openPicker.ts index a0726c9553f7d..5ad7f221dc520 100644 --- a/test/utils/pickers/openPicker.ts +++ b/test/utils/pickers/openPicker.ts @@ -8,7 +8,7 @@ export type OpenPickerParams = variant: 'mobile' | 'desktop'; } | { - type: 'date-range'; + type: 'date-range' | 'date-time-range'; variant: 'mobile' | 'desktop'; initialFocus: 'start' | 'end'; /** @@ -18,11 +18,12 @@ export type OpenPickerParams = }; export const openPicker = (params: OpenPickerParams) => { + const isRangePicker = params.type === 'date-range' || params.type === 'date-time-range'; const fieldSectionsContainer = getFieldSectionsContainer( - params.type === 'date-range' && !params.isSingleInput && params.initialFocus === 'end' ? 1 : 0, + isRangePicker && !params.isSingleInput && params.initialFocus === 'end' ? 1 : 0, ); - if (params.type === 'date-range') { + if (isRangePicker) { userEvent.mousePress(fieldSectionsContainer); if (params.isSingleInput && params.initialFocus === 'end') { From 89990fa2fc914b8d0b0ea34d01f5a743693a243c Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 27 Feb 2024 15:20:38 +0200 Subject: [PATCH 04/18] fix TS --- test/utils/pickers/assertions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/pickers/assertions.ts b/test/utils/pickers/assertions.ts index e9ce9c7288d8d..b66dd0ee095ed 100644 --- a/test/utils/pickers/assertions.ts +++ b/test/utils/pickers/assertions.ts @@ -30,11 +30,11 @@ export const expectFieldPlaceholderV6 = ( }; export function expectPickerChangeHandlerValue( - type: 'date' | 'date-time' | 'time' | 'date-range', + type: 'date' | 'date-time' | 'time' | 'date-range' | 'date-time-range', spyCallback: SinonSpy, expectedValue: any, ) { - if (type === 'date-range') { + if (['date-range', 'date-time-range'].includes(type)) { spyCallback.lastCall.firstArg.forEach((value, index) => { expect(value).to.deep.equal(expectedValue[index]); }); From 76e68267958297136f2463c7b71cfac13df9aa21 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 27 Feb 2024 15:29:41 +0200 Subject: [PATCH 05/18] Use `data-testid` for toolbar tests --- .../pickers/describePicker/describePicker.tsx | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/test/utils/pickers/describePicker/describePicker.tsx b/test/utils/pickers/describePicker/describePicker.tsx index 7752ded4ed3ff..aee9affd86066 100644 --- a/test/utils/pickers/describePicker/describePicker.tsx +++ b/test/utils/pickers/describePicker/describePicker.tsx @@ -134,23 +134,33 @@ function innerDescribePicker(ElementToTest: React.ElementType, options: Describe this.skip(); } - render(); + render( + , + ); if (variant === 'desktop') { - expect(screen.queryByMuiTest('picker-toolbar')).to.equal(null); + expect(screen.queryByTestId('pickers-toolbar')).to.equal(null); } else { - expect(screen.getByMuiTest('picker-toolbar')).toBeVisible(); + expect(screen.getByTestId('pickers-toolbar')).toBeVisible(); } }); - it('should render toolbar when `hidden` is `false`', function test() { + it.skip('should render toolbar when `hidden` is `false`', function test() { if (hasNoView) { this.skip(); } - render(); + render( + , + ); - expect(screen.getByMuiTest('picker-toolbar')).toBeVisible(); + expect(screen.getByTestId('pickers-toolbar')).toBeVisible(); }); it('should not render toolbar when `hidden` is `true`', function test() { @@ -158,9 +168,14 @@ function innerDescribePicker(ElementToTest: React.ElementType, options: Describe this.skip(); } - render(); + render( + , + ); - expect(screen.queryByMuiTest('picker-toolbar')).to.equal(null); + expect(screen.queryByTestId('pickers-toolbar')).to.equal(null); }); }); } From 7b42f03850805b4852b7cdba0b8e3e94b25e2407 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 27 Feb 2024 15:34:29 +0200 Subject: [PATCH 06/18] Fix to not render `DateTimeRangePickerToolbar` when `hidden` --- .../src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx b/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx index 858bdfa0336e2..8705b40a97277 100644 --- a/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx +++ b/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx @@ -182,6 +182,10 @@ const DateTimeRangePickerToolbar = React.forwardRef(function DateTimeRangePicker [onChange, onRangePositionChange, props.value, rangePosition, utils], ); + if (hidden) { + return null; + } + return ( Date: Tue, 27 Feb 2024 18:01:55 +0200 Subject: [PATCH 07/18] Add `aria-labelledBy` toolbar behavior to `MobileDateTimeRangePicker` --- .../src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx | 3 ++- .../hooks/useMobileRangePicker/useMobileRangePicker.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx b/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx index 8705b40a97277..32fbf3ec18ba1 100644 --- a/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx +++ b/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePickerToolbar.tsx @@ -133,7 +133,6 @@ const DateTimeRangePickerToolbar = React.forwardRef(function DateTimeRangePicker hidden, toolbarFormat, toolbarPlaceholder, - titleId, }; const localeText = useLocaleText(); @@ -203,6 +202,7 @@ const DateTimeRangePickerToolbar = React.forwardRef(function DateTimeRangePicker view={rangePosition === 'start' ? view : undefined} className={classes.startToolbar} onChange={handleOnChange} + titleId={titleId ? `${titleId}-start-toolbar` : undefined} {...commonToolbarProps} /> @@ -214,6 +214,7 @@ const DateTimeRangePickerToolbar = React.forwardRef(function DateTimeRangePicker view={rangePosition === 'end' ? view : undefined} className={classes.endToolbar} onChange={handleOnChange} + titleId={titleId ? `${titleId}-end-toolbar` : undefined} {...commonToolbarProps} /> diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx b/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx index b0095be48249f..ceef5d7e936e8 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx @@ -196,7 +196,8 @@ export const useMobileRangePicker = < ...contextLocaleText, ...localeText, }; - let labelledById = labelId; + let labelledById = + pickerParams.valueType === 'date-time' ? `${labelId}-start-toolbar ${labelId}-end-toolbar` : labelId; if (isToolbarHidden) { const labels: string[] = []; if (fieldType === 'multi-input') { From 4bf62cae06a5d504011b615ee5eeaa31e4ee37bb Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 27 Feb 2024 18:04:42 +0200 Subject: [PATCH 08/18] Add `MobileDateTimeRangePicker` describe tests --- ...scribes.MobileDateTimeRangePicker.test.tsx | 157 ++++++++++++++++++ .../testControlledUnControlled.tsx | 6 +- 2 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/tests/describes.MobileDateTimeRangePicker.test.tsx diff --git a/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/tests/describes.MobileDateTimeRangePicker.test.tsx b/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/tests/describes.MobileDateTimeRangePicker.test.tsx new file mode 100644 index 0000000000000..8b7f439ca1682 --- /dev/null +++ b/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/tests/describes.MobileDateTimeRangePicker.test.tsx @@ -0,0 +1,157 @@ +import * as React from 'react'; +import { describeConformance, fireEvent, screen, userEvent } from '@mui-internal/test-utils'; +import { + createPickerRenderer, + adapterToUse, + expectFieldValueV7, + describeValue, + describePicker, + describeRangeValidation, + wrapPickerMount, + getFieldSectionsContainer, + openPicker, +} from 'test/utils/pickers'; +import { MobileDateTimeRangePicker } from '../MobileDateTimeRangePicker'; + +describe(' - Describes', () => { + const { render, clock } = createPickerRenderer({ + clock: 'fake', + }); + + describePicker(MobileDateTimeRangePicker, { + render, + fieldType: 'multi-input', + variant: 'mobile', + }); + + describeRangeValidation(MobileDateTimeRangePicker, () => ({ + render, + clock, + views: ['day', 'hours', 'minutes'], + componentFamily: 'picker', + variant: 'mobile', + })); + + describeConformance(, () => ({ + classes: {} as any, + render, + muiName: 'MuiMobileDateTimeRangePicker', + wrapMount: wrapPickerMount, + refInstanceof: window.HTMLDivElement, + skip: [ + 'componentProp', + 'componentsProp', + 'themeDefaultProps', + 'themeStyleOverrides', + 'themeVariants', + 'mergeClassName', + 'propsSpread', + 'rootClass', + 'reactTestRenderer', + ], + })); + + describeValue(MobileDateTimeRangePicker, () => ({ + render, + componentFamily: 'picker', + type: 'date-time-range', + variant: 'mobile', + initialFocus: 'start', + clock, + values: [ + // initial start and end dates + [adapterToUse.date('2018-01-01T11:30:00'), adapterToUse.date('2018-01-04T11:45:00')], + // start and end dates after `setNewValue` + [adapterToUse.date('2018-01-02T12:35:00'), adapterToUse.date('2018-01-05T12:50:00')], + ], + emptyValue: [null, null], + assertRenderedValue: (expectedValues: any[]) => { + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + const expectedPlaceholder = hasMeridiem ? 'MM/DD/YYYY hh:mm aa' : 'MM/DD/YYYY hh:mm'; + + const startSectionsContainer = getFieldSectionsContainer(0); + const expectedStartValueStr = expectedValues[0] + ? adapterToUse.format( + expectedValues[0], + hasMeridiem ? 'keyboardDateTime12h' : 'keyboardDateTime24h', + ) + : expectedPlaceholder; + expectFieldValueV7(startSectionsContainer, expectedStartValueStr); + + const endSectionsContainer = getFieldSectionsContainer(1); + const expectedEndValueStr = expectedValues[1] + ? adapterToUse.format( + expectedValues[1], + hasMeridiem ? 'keyboardDateTime12h' : 'keyboardDateTime24h', + ) + : expectedPlaceholder; + expectFieldValueV7(endSectionsContainer, expectedEndValueStr); + }, + setNewValue: (value, { isOpened, applySameValue, setEndDate = false }) => { + if (!isOpened) { + openPicker({ + type: 'date-time-range', + variant: 'mobile', + initialFocus: setEndDate ? 'end' : 'start', + }); + } + let newValue: any[]; + if (applySameValue) { + newValue = value; + } else if (setEndDate) { + newValue = [ + value[0], + adapterToUse.addMinutes(adapterToUse.addHours(adapterToUse.addDays(value[1], 1), 1), 5), + ]; + } else { + newValue = [ + adapterToUse.addMinutes(adapterToUse.addHours(adapterToUse.addDays(value[0], 1), 1), 5), + value[1], + ]; + } + + // if we want to set the end date, we firstly need to switch to end date "range position" + if (setEndDate) { + userEvent.mousePress( + screen.getByRole('button', { name: adapterToUse.format(value[1], 'shortDate') }), + ); + } + + userEvent.mousePress( + screen.getByRole('gridcell', { + name: adapterToUse.getDate(newValue[setEndDate ? 1 : 0]).toString(), + }), + ); + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + const hours = adapterToUse.format( + newValue[setEndDate ? 1 : 0], + hasMeridiem ? 'hours12h' : 'hours24h', + ); + const hoursNumber = adapterToUse.getHours(newValue[setEndDate ? 1 : 0]); + userEvent.mousePress(screen.getByRole('option', { name: `${parseInt(hours, 10)} hours` })); + userEvent.mousePress( + screen.getByRole('option', { + name: `${adapterToUse.getMinutes(newValue[setEndDate ? 1 : 0])} minutes`, + }), + ); + if (hasMeridiem) { + // meridiem is an extra view on `MobileDateTimeRangePicker` + // we need to click it to finish selection + userEvent.mousePress(screen.getByRole('option', { name: hoursNumber >= 12 ? 'PM' : 'AM' })); + } + // Close the picker + if (!isOpened) { + // eslint-disable-next-line material-ui/disallow-active-element-as-key-event-target + fireEvent.keyDown(document.activeElement!, { key: 'Escape' }); + clock.runToLast(); + } else { + // return to the start date view in case we'd like to repeat the selection process + userEvent.mousePress( + screen.getByRole('button', { name: adapterToUse.format(newValue[0], 'shortDate') }), + ); + } + + return newValue; + }, + })); +}); diff --git a/test/utils/pickers/describeValue/testControlledUnControlled.tsx b/test/utils/pickers/describeValue/testControlledUnControlled.tsx index b173f3e0e0ee2..29e4c3f5e5553 100644 --- a/test/utils/pickers/describeValue/testControlledUnControlled.tsx +++ b/test/utils/pickers/describeValue/testControlledUnControlled.tsx @@ -184,7 +184,11 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( localeText: { toolbarTitle: 'Test toolbar' }, }); - expect(screen.getByLabelText('Test toolbar')).to.have.attribute('role', 'dialog'); + if (params.variant === 'mobile' && params.type === 'date-time-range') { + expect(screen.getByLabelText('Start End')).to.have.attribute('role', 'dialog'); + } else { + expect(screen.getByLabelText('Test toolbar')).to.have.attribute('role', 'dialog'); + } }); it('should have correct labelledby relationship with provided label when toolbar is hidden', () => { From dcf586075336b6f314917ff4d58220820bd5e1e1 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 27 Feb 2024 18:06:39 +0200 Subject: [PATCH 09/18] Add `DateTimeRangePicker` describes --- .../tests/describes.DateRangePicker.test.tsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 packages/x-date-pickers-pro/src/DateTimeRangePicker/tests/describes.DateRangePicker.test.tsx diff --git a/packages/x-date-pickers-pro/src/DateTimeRangePicker/tests/describes.DateRangePicker.test.tsx b/packages/x-date-pickers-pro/src/DateTimeRangePicker/tests/describes.DateRangePicker.test.tsx new file mode 100644 index 0000000000000..4778e65099b22 --- /dev/null +++ b/packages/x-date-pickers-pro/src/DateTimeRangePicker/tests/describes.DateRangePicker.test.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { createPickerRenderer, wrapPickerMount } from 'test/utils/pickers'; +import { describeConformance } from 'test/utils/describeConformance'; +import { DateTimeRangePicker } from '../DateTimeRangePicker'; + +describe(' - Describes', () => { + const { render } = createPickerRenderer({ clock: 'fake' }); + + describeConformance(, () => ({ + classes: {} as any, + render, + muiName: 'MuiDateTimeRangePicker', + wrapMount: wrapPickerMount, + refInstanceof: window.HTMLDivElement, + skip: [ + 'componentProp', + 'componentsProp', + 'themeDefaultProps', + 'themeStyleOverrides', + 'themeVariants', + 'mergeClassName', + 'propsSpread', + 'rootClass', + 'reactTestRenderer', + ], + })); +}); From 97216298229efff3ae37bc995055ba6c76b1802d Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 27 Feb 2024 18:06:56 +0200 Subject: [PATCH 10/18] prettier --- .../hooks/useMobileRangePicker/useMobileRangePicker.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx b/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx index ceef5d7e936e8..e0016ea48423a 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx @@ -197,7 +197,9 @@ export const useMobileRangePicker = < ...localeText, }; let labelledById = - pickerParams.valueType === 'date-time' ? `${labelId}-start-toolbar ${labelId}-end-toolbar` : labelId; + pickerParams.valueType === 'date-time' + ? `${labelId}-start-toolbar ${labelId}-end-toolbar` + : labelId; if (isToolbarHidden) { const labels: string[] = []; if (fieldType === 'multi-input') { From bcaf2ffd0ffa5eb77b39f2e22e35705b04ad0c72 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 27 Feb 2024 18:11:58 +0200 Subject: [PATCH 11/18] Fix test file name --- ...angePicker.test.tsx => describes.DateTimeRangePicker.test.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/x-date-pickers-pro/src/DateTimeRangePicker/tests/{describes.DateRangePicker.test.tsx => describes.DateTimeRangePicker.test.tsx} (100%) diff --git a/packages/x-date-pickers-pro/src/DateTimeRangePicker/tests/describes.DateRangePicker.test.tsx b/packages/x-date-pickers-pro/src/DateTimeRangePicker/tests/describes.DateTimeRangePicker.test.tsx similarity index 100% rename from packages/x-date-pickers-pro/src/DateTimeRangePicker/tests/describes.DateRangePicker.test.tsx rename to packages/x-date-pickers-pro/src/DateTimeRangePicker/tests/describes.DateTimeRangePicker.test.tsx From 9f986f2b4a7114bf4b142d7bbc5f1ab75fd457a3 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 27 Feb 2024 18:18:14 +0200 Subject: [PATCH 12/18] docs:api --- .../api/date-pickers/date-time-range-picker.json | 3 +++ .../desktop-date-time-range-picker.json | 3 +++ .../date-pickers/mobile-date-range-picker.json | 12 ++++++------ .../mobile-date-time-range-picker.json | 15 +++++++++------ 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/pages/x/api/date-pickers/date-time-range-picker.json b/docs/pages/x/api/date-pickers/date-time-range-picker.json index d08722fb1fd14..6980f341feb1b 100644 --- a/docs/pages/x/api/date-pickers/date-time-range-picker.json +++ b/docs/pages/x/api/date-pickers/date-time-range-picker.json @@ -367,7 +367,10 @@ } ], "classes": [], + "spread": false, + "themeDefaultProps": false, "muiName": "MuiDateTimeRangePicker", + "forwardsRefTo": "HTMLDivElement", "filename": "/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.tsx", "inheritance": null, "demos": "", diff --git a/docs/pages/x/api/date-pickers/desktop-date-time-range-picker.json b/docs/pages/x/api/date-pickers/desktop-date-time-range-picker.json index 5370342674072..2ffc29c59437b 100644 --- a/docs/pages/x/api/date-pickers/desktop-date-time-range-picker.json +++ b/docs/pages/x/api/date-pickers/desktop-date-time-range-picker.json @@ -345,7 +345,10 @@ { "name": "field", "description": "", "class": null } ], "classes": [], + "spread": false, + "themeDefaultProps": false, "muiName": "MuiDesktopDateTimeRangePicker", + "forwardsRefTo": "HTMLDivElement", "filename": "/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx", "inheritance": null, "demos": "", diff --git a/docs/pages/x/api/date-pickers/mobile-date-range-picker.json b/docs/pages/x/api/date-pickers/mobile-date-range-picker.json index 2c0481a80fd63..4e4b02c80de7f 100644 --- a/docs/pages/x/api/date-pickers/mobile-date-range-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-date-range-picker.json @@ -200,6 +200,12 @@ "default": "ArrowDropDown", "class": null }, + { + "name": "dialog", + "description": "Custom component for the dialog inside which the views are rendered on mobile.", + "default": "PickersModalDialogRoot", + "class": null + }, { "name": "actionBar", "description": "Custom component for the action bar, it is placed below the picker views.", @@ -245,12 +251,6 @@ "default": "IconButton", "class": null }, - { - "name": "dialog", - "description": "Custom component for the dialog inside which the views are rendered on mobile.", - "default": "PickersModalDialogRoot", - "class": null - }, { "name": "mobilePaper", "description": "Custom component for the paper rendered inside the mobile picker's Dialog.", diff --git a/docs/pages/x/api/date-pickers/mobile-date-time-range-picker.json b/docs/pages/x/api/date-pickers/mobile-date-time-range-picker.json index 4b62aadfa72e5..21c66292a2a0f 100644 --- a/docs/pages/x/api/date-pickers/mobile-date-time-range-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-date-time-range-picker.json @@ -269,6 +269,12 @@ "default": "MenuItem from '@mui/material'", "class": null }, + { + "name": "dialog", + "description": "Custom component for the dialog inside which the views are rendered on mobile.", + "default": "PickersModalDialogRoot", + "class": null + }, { "name": "actionBar", "description": "Custom component for the action bar, it is placed below the picker views.", @@ -314,12 +320,6 @@ "default": "IconButton", "class": null }, - { - "name": "dialog", - "description": "Custom component for the dialog inside which the views are rendered on mobile.", - "default": "PickersModalDialogRoot", - "class": null - }, { "name": "mobilePaper", "description": "Custom component for the paper rendered inside the mobile picker's Dialog.", @@ -335,7 +335,10 @@ { "name": "field", "description": "", "class": null } ], "classes": [], + "spread": false, + "themeDefaultProps": false, "muiName": "MuiMobileDateTimeRangePicker", + "forwardsRefTo": "HTMLDivElement", "filename": "/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx", "inheritance": null, "demos": "", From d4825a9c22d5c98d2ae3dd48b77d2d83d297f9d9 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 27 Feb 2024 18:40:08 +0200 Subject: [PATCH 13/18] Add `aria-disabled` to disabled `MultiSectionDigitalClock` section item --- .../MultiSectionDigitalClockSection.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx index ced796ed21fad..504d2cf7c5215 100644 --- a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx @@ -187,7 +187,9 @@ export const MultiSectionDigitalClockSection = React.forwardRef( {...other} > {items.map((option, index) => { - if (skipDisabled && option.isDisabled?.(option.value)) { + const isItemDisabled = option.isDisabled?.(option.value); + const isDisabled = disabled || isItemDisabled; + if (skipDisabled && isItemDisabled) { return null; } const isSelected = option.isSelected(option.value); @@ -198,11 +200,11 @@ export const MultiSectionDigitalClockSection = React.forwardRef( key={option.label} onClick={() => !readOnly && onChange(option.value)} selected={isSelected} - disabled={disabled || option.isDisabled?.(option.value)} + disabled={isDisabled} disableRipple={readOnly} role="option" // aria-readonly is not supported here and does not have any effect - aria-disabled={readOnly} + aria-disabled={readOnly || isDisabled || undefined} aria-label={option.ariaLabel} aria-selected={isSelected} tabIndex={tabIndex} From 2f5f85108ecd4790eab03a6a4ddb89be4a82cf8d Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 28 Feb 2024 13:59:44 +0200 Subject: [PATCH 14/18] Add `DesktopDateTimeRangePicker` tests --- .../tests/DesktopDateTimeRangePicker.test.tsx | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx diff --git a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx new file mode 100644 index 0000000000000..f824646023a44 --- /dev/null +++ b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { screen } from '@mui-internal/test-utils'; +import { createPickerRenderer, adapterToUse } from 'test/utils/pickers'; +import { DesktopDateTimeRangePicker } from '../DesktopDateTimeRangePicker'; + +describe('', () => { + const { render } = createPickerRenderer({ + clock: 'fake', + clockConfig: new Date(2018, 0, 10, 10, 16, 0), + }); + + describe('disabled dates', () => { + it('should respect the "disablePast" prop', () => { + render(); + + expect(screen.getByRole('gridcell', { name: '8' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '9' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '10' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '11' })).not.to.have.attribute('disabled'); + + expect(screen.getByRole('option', { name: '9 hours' })).to.have.attribute( + 'aria-disabled', + 'true', + ); + expect(screen.getByRole('option', { name: '10 hours' })).not.to.have.attribute( + 'aria-disabled', + ); + + expect(screen.getByRole('option', { name: '15 minutes' })).to.have.attribute( + 'aria-disabled', + 'true', + ); + }); + + it('should respect the "disableFuture" prop', () => { + render(); + + expect(screen.getByRole('gridcell', { name: '9' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '10' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '11' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '12' })).to.have.attribute('disabled'); + + expect(screen.getByRole('option', { name: '10 hours' })).not.to.have.attribute( + 'aria-disabled', + ); + expect(screen.getByRole('option', { name: '11 hours' })).to.have.attribute( + 'aria-disabled', + 'true', + ); + }); + + it('should respect the "minDateTime" prop', () => { + render( + , + ); + + expect(screen.getByRole('gridcell', { name: '8' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '9' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '10' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '11' })).not.to.have.attribute('disabled'); + + expect(screen.getByRole('option', { name: '9 hours' })).to.have.attribute( + 'aria-disabled', + 'true', + ); + expect(screen.getByRole('option', { name: '10 hours' })).not.to.have.attribute( + 'aria-disabled', + ); + + expect(screen.getByRole('option', { name: '15 minutes' })).to.have.attribute( + 'aria-disabled', + 'true', + ); + expect(screen.getByRole('option', { name: '20 minutes' })).not.to.have.attribute( + 'aria-disabled', + ); + }); + + it('should respect the "maxDateTime" prop', () => { + render( + , + ); + + expect(screen.getByRole('gridcell', { name: '9' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '10' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '11' })).to.have.attribute('disabled'); + + expect(screen.getByRole('option', { name: '10 hours' })).not.to.have.attribute( + 'aria-disabled', + ); + expect(screen.getByRole('option', { name: '11 hours' })).to.have.attribute( + 'aria-disabled', + 'true', + ); + + expect(screen.getByRole('option', { name: '15 minutes' })).not.to.have.attribute( + 'aria-disabled', + ); + expect(screen.getByRole('option', { name: '20 minutes' })).to.have.attribute( + 'aria-disabled', + 'true', + ); + }); + }); +}); From 1cf0b5034504f75dcef79f8f6afa7e66fe3c0238 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 28 Feb 2024 14:19:39 +0200 Subject: [PATCH 15/18] Add `disablePast` and `disableFuture` tests combined with different reference date --- .../tests/DesktopDateTimeRangePicker.test.tsx | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx index f824646023a44..c216375579bd2 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx +++ b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx @@ -33,6 +33,33 @@ describe('', () => { ); }); + it('should respect the "disablePast" prop combined with "referenceDate"', () => { + render( + , + ); + + expect(screen.getByRole('gridcell', { name: '8' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '9' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '10' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '11' })).not.to.have.attribute('disabled'); + + expect(screen.getByRole('option', { name: '9 hours' })).not.to.have.attribute( + 'aria-disabled', + ); + expect(screen.getByRole('option', { name: '10 hours' })).not.to.have.attribute( + 'aria-disabled', + ); + + expect(screen.getByRole('option', { name: '15 minutes' })).not.to.have.attribute( + 'aria-disabled', + ); + }); + it('should respect the "disableFuture" prop', () => { render(); @@ -50,6 +77,29 @@ describe('', () => { ); }); + it('should respect the "disableFuture" prop combined with "referenceDate"', () => { + render( + , + ); + + expect(screen.getByRole('gridcell', { name: '9' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '10' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '11' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '12' })).to.have.attribute('disabled'); + + expect(screen.getByRole('option', { name: '10 hours' })).not.to.have.attribute( + 'aria-disabled', + ); + expect(screen.getByRole('option', { name: '11 hours' })).not.to.have.attribute( + 'aria-disabled', + ); + }); + it('should respect the "minDateTime" prop', () => { render( Date: Wed, 28 Feb 2024 14:20:40 +0200 Subject: [PATCH 16/18] Add GH issue link --- .../tests/DesktopDateTimeRangePicker.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx index c216375579bd2..793d2a6682d92 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx +++ b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/tests/DesktopDateTimeRangePicker.test.tsx @@ -33,6 +33,7 @@ describe('', () => { ); }); + // Asserts correct behavior: https://github.com/mui/mui-x/issues/12048 it('should respect the "disablePast" prop combined with "referenceDate"', () => { render( ', () => { ); }); + // Asserts correct behavior: https://github.com/mui/mui-x/issues/12048 it('should respect the "disableFuture" prop combined with "referenceDate"', () => { render( Date: Wed, 28 Feb 2024 16:43:25 +0200 Subject: [PATCH 17/18] Code review: Michel --- .../describeRangeValidation/testDayViewRangeValidation.tsx | 4 ++-- .../pickers/describeValue/testControlledUnControlled.tsx | 7 ++----- test/utils/pickers/describeValue/testPickerActionBar.tsx | 2 +- .../pickers/describeValue/testPickerOpenCloseLifeCycle.tsx | 2 +- test/utils/pickers/openPicker.ts | 6 +++--- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/test/utils/pickers/describeRangeValidation/testDayViewRangeValidation.tsx b/test/utils/pickers/describeRangeValidation/testDayViewRangeValidation.tsx index 78c0c987506ca..fc1a3865ab935 100644 --- a/test/utils/pickers/describeRangeValidation/testDayViewRangeValidation.tsx +++ b/test/utils/pickers/describeRangeValidation/testDayViewRangeValidation.tsx @@ -3,10 +3,10 @@ import { expect } from 'chai'; import { screen } from '@mui-internal/test-utils'; import { adapterToUse } from 'test/utils/pickers'; -const isDisable = (el: HTMLElement) => el.getAttribute('disabled') !== null; +const isDisabled = (el: HTMLElement) => el.getAttribute('disabled') !== null; const testDisabledDate = (day: string, expectedAnswer: boolean[], isSingleCalendar: boolean) => { - expect(screen.getAllByRole('gridcell', { name: day }).map(isDisable)).to.deep.equal( + expect(screen.getAllByRole('gridcell', { name: day }).map(isDisabled)).to.deep.equal( isSingleCalendar ? expectedAnswer.slice(0, 1) : expectedAnswer, ); }; diff --git a/test/utils/pickers/describeValue/testControlledUnControlled.tsx b/test/utils/pickers/describeValue/testControlledUnControlled.tsx index 29e4c3f5e5553..493b9c66cce40 100644 --- a/test/utils/pickers/describeValue/testControlledUnControlled.tsx +++ b/test/utils/pickers/describeValue/testControlledUnControlled.tsx @@ -28,7 +28,7 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( const params = pickerParams as DescribeValueOptions<'picker', any>; - const isRangeType = ['date-range', 'date-time-range'].includes(params.type); + const isRangeType = params.type === 'date-range' || params.type === 'date-time-range'; const isDesktopRange = params.variant === 'desktop' && isRangeType; describe('Controlled / uncontrolled value', () => { @@ -257,10 +257,7 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( expect(fieldRoot).to.have.class(inputBaseClasses.error); expect(fieldRoot).to.have.attribute('aria-invalid', 'true'); - if ( - (params.type === 'date-range' || params.type === 'date-time-range') && - !params.isSingleInput - ) { + if (isRangeType && !params.isSingleInput) { const fieldRootEnd = getFieldInputRoot(1); expect(fieldRootEnd).to.have.class(inputBaseClasses.error); expect(fieldRootEnd).to.have.attribute('aria-invalid', 'true'); diff --git a/test/utils/pickers/describeValue/testPickerActionBar.tsx b/test/utils/pickers/describeValue/testPickerActionBar.tsx index 4070b908da148..c58458a917764 100644 --- a/test/utils/pickers/describeValue/testPickerActionBar.tsx +++ b/test/utils/pickers/describeValue/testPickerActionBar.tsx @@ -27,7 +27,7 @@ export const testPickerActionBar: DescribeValueTestSuite = ( return; } - const isRangeType = ['date-range', 'date-time-range'].includes(pickerParams.type); + const isRangeType = pickerParams.type === 'date-range' || pickerParams.type === 'date-time-range'; describe('Picker action bar', () => { describe('clear action', () => { diff --git a/test/utils/pickers/describeValue/testPickerOpenCloseLifeCycle.tsx b/test/utils/pickers/describeValue/testPickerOpenCloseLifeCycle.tsx index e376828605274..24ef112fc206b 100644 --- a/test/utils/pickers/describeValue/testPickerOpenCloseLifeCycle.tsx +++ b/test/utils/pickers/describeValue/testPickerOpenCloseLifeCycle.tsx @@ -16,7 +16,7 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite return; } - const isRangeType = ['date-range', 'date-time-range'].includes(pickerParams.type); + const isRangeType = pickerParams.type === 'date-range' || pickerParams.type === 'date-time-range'; const viewWrapperRole = isRangeType && pickerParams.variant === 'desktop' ? 'tooltip' : 'dialog'; describe('Picker open / close lifecycle', () => { diff --git a/test/utils/pickers/openPicker.ts b/test/utils/pickers/openPicker.ts index 5ad7f221dc520..2de6210fca38c 100644 --- a/test/utils/pickers/openPicker.ts +++ b/test/utils/pickers/openPicker.ts @@ -18,12 +18,12 @@ export type OpenPickerParams = }; export const openPicker = (params: OpenPickerParams) => { - const isRangePicker = params.type === 'date-range' || params.type === 'date-time-range'; + const isRangeType = params.type === 'date-range' || params.type === 'date-time-range'; const fieldSectionsContainer = getFieldSectionsContainer( - isRangePicker && !params.isSingleInput && params.initialFocus === 'end' ? 1 : 0, + isRangeType && !params.isSingleInput && params.initialFocus === 'end' ? 1 : 0, ); - if (isRangePicker) { + if (isRangeType) { userEvent.mousePress(fieldSectionsContainer); if (params.isSingleInput && params.initialFocus === 'end') { From 54ae9771d11c76d573be8a068c49442758cd701d Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 28 Feb 2024 16:43:50 +0200 Subject: [PATCH 18/18] Check for global `disabled` only once --- .../MultiSectionDigitalClock/MultiSectionDigitalClock.tsx | 7 +++---- .../MultiSectionDigitalClockSection.tsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx index 3aae2c8dcb262..5c309e3f02e48 100644 --- a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx @@ -292,7 +292,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi value, ampm, utils, - isDisabled: (hours) => disabled || isTimeDisabled(hours, 'hours'), + isDisabled: (hours) => isTimeDisabled(hours, 'hours'), timeStep: timeSteps.hours, resolveAriaLabel: localeText.hoursClockNumberText, valueOrReferenceDate, @@ -312,7 +312,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi items: getTimeSectionOptions({ value: utils.getMinutes(valueOrReferenceDate), utils, - isDisabled: (minutes) => disabled || isTimeDisabled(minutes, 'minutes'), + isDisabled: (minutes) => isTimeDisabled(minutes, 'minutes'), resolveLabel: (minutes) => utils.format(utils.setMinutes(now, minutes), 'minutes'), timeStep: timeSteps.minutes, hasValue: !!value, @@ -333,7 +333,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi items: getTimeSectionOptions({ value: utils.getSeconds(valueOrReferenceDate), utils, - isDisabled: (seconds) => disabled || isTimeDisabled(seconds, 'seconds'), + isDisabled: (seconds) => isTimeDisabled(seconds, 'seconds'), resolveLabel: (seconds) => utils.format(utils.setSeconds(now, seconds), 'seconds'), timeStep: timeSteps.seconds, hasValue: !!value, @@ -384,7 +384,6 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi meridiemMode, setValueAndGoToNextView, valueOrReferenceDate, - disabled, isTimeDisabled, handleMeridiemChange, ], diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx index 504d2cf7c5215..ced9449549135 100644 --- a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx @@ -189,7 +189,7 @@ export const MultiSectionDigitalClockSection = React.forwardRef( {items.map((option, index) => { const isItemDisabled = option.isDisabled?.(option.value); const isDisabled = disabled || isItemDisabled; - if (skipDisabled && isItemDisabled) { + if (skipDisabled && isDisabled) { return null; } const isSelected = option.isSelected(option.value);