Skip to content

Commit

Permalink
[pickers] Refine referenceDate behavior in views (#10863)
Browse files Browse the repository at this point in the history
  • Loading branch information
LukasTy authored Nov 9, 2023
1 parent b5a027f commit c7682ff
Show file tree
Hide file tree
Showing 21 changed files with 169 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,17 @@ describe('<DateCalendar />', () => {
render(
<DateCalendar
onChange={onChange}
referenceDate={adapterToUse.date(new Date(2018, 0, 1, 12, 30))}
referenceDate={adapterToUse.date(new Date(2022, 3, 17, 12, 30))}
view="day"
/>,
);

// should make the reference day firstly focusable
expect(screen.getByRole('gridcell', { name: '17' })).to.have.attribute('tabindex', '0');

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

it('should not use `referenceDate` when a value is defined', () => {
Expand Down
5 changes: 2 additions & 3 deletions packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import useEventCallback from '@mui/utils/useEventCallback';
import { SlideDirection } from './PickersSlideTransition';
import { useIsDateDisabled } from './useIsDateDisabled';
import { useUtils, useNow } from '../internals/hooks/useUtils';
import { useUtils } from '../internals/hooks/useUtils';
import { MuiPickersAdapter, PickersTimezone } from '../models';
import { DateCalendarDefaultizedProps } from './DateCalendar.types';
import { singleItemValueManager } from '../internals/utils/valueManagers';
Expand Down Expand Up @@ -127,7 +127,6 @@ export const useCalendarState = <TDate extends unknown>(params: UseCalendarState
timezone,
} = params;

const now = useNow<TDate>(timezone);
const utils = useUtils<TDate>();

const reducerFn = React.useRef(
Expand Down Expand Up @@ -162,7 +161,7 @@ export const useCalendarState = <TDate extends unknown>(params: UseCalendarState

const [calendarState, dispatch] = React.useReducer(reducerFn, {
isMonthSwitchingAnimating: false,
focusedDay: value || now,
focusedDay: referenceDate,
currentMonth: utils.startOfMonth(referenceDate),
slideDirection: 'left',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
describeValidation,
describeValue,
describePicker,
formatFullTimeValue,
} from 'test/utils/pickers';
import { DesktopTimePicker } from '@mui/x-date-pickers/DesktopTimePicker';

Expand Down Expand Up @@ -68,9 +69,7 @@ describe('<DesktopTimePicker /> - Describes', () => {
}
expectInputValue(
input,
expectedValue
? adapterToUse.format(expectedValue, hasMeridiem ? 'fullTime12h' : 'fullTime24h')
: '',
expectedValue ? formatFullTimeValue(adapterToUse, expectedValue) : '',
);
},
setNewValue: (value, { isOpened, applySameValue, selectSection }) => {
Expand Down
21 changes: 15 additions & 6 deletions packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,17 @@ export const DigitalClock = React.forwardRef(function DigitalClock<TDate extends
if (containerRef.current === null) {
return;
}
const selectedItem = containerRef.current.querySelector<HTMLElement>(
'[role="listbox"] [role="option"][aria-selected="true"]',
const activeItem = containerRef.current.querySelector<HTMLElement>(
'[role="listbox"] [role="option"][tabindex="0"], [role="listbox"] [role="option"][aria-selected="true"]',
);

if (!selectedItem) {
if (!activeItem) {
return;
}
const offsetTop = selectedItem.offsetTop;
const offsetTop = activeItem.offsetTop;
if (autoFocus || !!focusedView) {
activeItem.focus();
}

// Subtracting the 4px of extra margin intended for the first visible section item
containerRef.current.scrollTop = offsetTop - 4;
Expand Down Expand Up @@ -281,6 +284,10 @@ export const DigitalClock = React.forwardRef(function DigitalClock<TDate extends
];
}, [valueOrReferenceDate, timeStep, utils]);

const focusedOptionIndex = timeOptions.findIndex((option) =>
utils.isEqual(option, valueOrReferenceDate),
);

return (
<DigitalClockRoot
ref={handleRef}
Expand All @@ -289,16 +296,17 @@ export const DigitalClock = React.forwardRef(function DigitalClock<TDate extends
{...other}
>
<DigitalClockList
autoFocusItem={autoFocus || !!focusedView}
role="listbox"
aria-label={localeText.timePickerToolbarTitle}
className={classes.list}
>
{timeOptions.map((option) => {
{timeOptions.map((option, index) => {
if (skipDisabled && isTimeDisabled(option)) {
return null;
}
const isSelected = utils.isEqual(option, value);
const tabIndex =
focusedOptionIndex === index || (focusedOptionIndex === -1 && index === 0) ? 0 : -1;
return (
<ClockItem
key={utils.toISO(option)}
Expand All @@ -310,6 +318,7 @@ export const DigitalClock = React.forwardRef(function DigitalClock<TDate extends
// aria-readonly is not supported here and does not have any effect
aria-disabled={readOnly}
aria-selected={isSelected}
tabIndex={tabIndex}
{...clockItemProps}
>
{utils.format(option, ampm ? 'fullTime12h' : 'fullTime24h')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,36 @@ import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { DigitalClock } from '@mui/x-date-pickers/DigitalClock';
import { adapterToUse, createPickerRenderer, digitalClockHandler } from 'test/utils/pickers';
import {
adapterToUse,
createPickerRenderer,
digitalClockHandler,
formatFullTimeValue,
} from 'test/utils/pickers';
import { screen } from '@mui-internal/test-utils';

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

describe('Reference date', () => {
it('should use `referenceDate` when no value defined', () => {
const onChange = spy();
const referenceDate = new Date(2018, 0, 1, 12, 30);

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

// the first item should not be initially focusable when `referenceDate` is defined
expect(
screen.getByRole('option', {
name: formatFullTimeValue(adapterToUse, new Date(2018, 0, 1, 0, 0, 0)),
}),
).to.have.attribute('tabindex', '-1');
// check that the relevant time based on the `referenceDate` is focusable
expect(
screen.getByRole('option', {
name: formatFullTimeValue(adapterToUse, referenceDate),
}),
).to.have.attribute('tabindex', '0');

digitalClockHandler.setViewValue(
adapterToUse,
Expand All @@ -26,6 +41,18 @@ describe('<DigitalClock />', () => {
expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2018, 0, 1, 15, 30));
});

it('should fallback to making the first entry focusable when `referenceDate` does not map to any option', () => {
const referenceDate = new Date(2018, 0, 1, 12, 33);

render(<DigitalClock referenceDate={adapterToUse.date(referenceDate)} />);

expect(
screen.getByRole('option', {
name: formatFullTimeValue(adapterToUse, new Date(2018, 0, 1, 0, 0, 0)),
}),
).to.have.attribute('tabindex', '0');
});

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
digitalClockHandler,
describeValidation,
describeValue,
formatFullTimeValue,
} from 'test/utils/pickers';
import { DigitalClock } from '@mui/x-date-pickers/DigitalClock';

Expand Down Expand Up @@ -56,14 +57,11 @@ describe('<DigitalClock /> - Describes', () => {
emptyValue: null,
clock,
assertRenderedValue: (expectedValue: any) => {
const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale();
const selectedItem = screen.queryByRole('option', { selected: true });
if (!expectedValue) {
expect(selectedItem).to.equal(null);
} else {
expect(selectedItem).to.have.text(
adapterToUse.format(expectedValue, hasMeridiem ? 'fullTime12h' : 'fullTime24h'),
);
expect(selectedItem).to.have.text(formatFullTimeValue(adapterToUse, expectedValue));
}
},
setNewValue: (value) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
describeValidation,
describeValue,
describePicker,
formatFullTimeValue,
} from 'test/utils/pickers';
import { MobileTimePicker } from '@mui/x-date-pickers/MobileTimePicker';

Expand Down Expand Up @@ -73,7 +74,7 @@ describe('<MobileTimePicker /> - Describes', () => {
expectInputPlaceholder(input, hasMeridiem ? 'hh:mm aa' : 'hh:mm');
}
const expectedValueStr = expectedValue
? adapterToUse.format(expectedValue, hasMeridiem ? 'fullTime12h' : 'fullTime24h')
? formatFullTimeValue(adapterToUse, expectedValue)
: '';

expectInputValue(input, expectedValueStr);
Expand Down
12 changes: 5 additions & 7 deletions packages/x-date-pickers/src/MonthCalendar/MonthCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,11 @@ export const MonthCalendar = React.forwardRef(function MonthCalendar<TDate>(
return utils.getMonth(value);
}

if (disableHighlightToday) {
return null;
}

return utils.getMonth(referenceDate);
}, [value, utils, disableHighlightToday, referenceDate]);
const [focusedMonth, setFocusedMonth] = React.useState(() => selectedMonth || todayMonth);
return null;
}, [value, utils]);
const [focusedMonth, setFocusedMonth] = React.useState(
() => selectedMonth || utils.getMonth(referenceDate),
);

const [internalHasFocus, setInternalHasFocus] = useControlled({
name: 'MonthCalendar',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,11 @@ describe('<MonthCalendar />', () => {
expect(january).not.to.have.attribute('disabled');
expect(february).to.have.attribute('disabled');
});

it('should not mark the `referenceDate` month as selected', () => {
render(<MonthCalendar referenceDate={adapterToUse.date(new Date(2018, 1, 2))} />);

expect(screen.getByRole('radio', { name: 'February', checked: false })).to.not.equal(null);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import {
describeValidation,
describeValue,
} from 'test/utils/pickers';
import {
MonthCalendar,
monthCalendarClasses as classes,
pickersMonthClasses,
} from '@mui/x-date-pickers/MonthCalendar';
import { MonthCalendar, monthCalendarClasses as classes } from '@mui/x-date-pickers/MonthCalendar';

describe('<MonthCalendar /> - Describes', () => {
const { render, clock } = createPickerRenderer({ clock: 'fake' });
Expand Down Expand Up @@ -42,17 +38,19 @@ describe('<MonthCalendar /> - Describes', () => {
emptyValue: null,
clock,
assertRenderedValue: (expectedValue: any) => {
const selectedCells = document.querySelectorAll(`.${pickersMonthClasses.selected}`);
const activeMonth = screen
.queryAllByRole('radio')
.find((cell) => cell.getAttribute('tabindex') === '0');
expect(activeMonth).not.to.equal(null);
if (expectedValue == null) {
expect(selectedCells).to.have.length(1);
expect(selectedCells[0]).to.have.text(
expect(activeMonth).to.have.text(
adapterToUse.format(adapterToUse.date(), 'monthShort').toString(),
);
} else {
expect(selectedCells).to.have.length(1);
expect(selectedCells[0]).to.have.text(
expect(activeMonth).to.have.text(
adapterToUse.format(expectedValue, 'monthShort').toString(),
);
expect(activeMonth).to.have.attribute('aria-checked', 'true');
}
},
setNewValue: (value) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi
isDisabled: (hours) => disabled || isTimeDisabled(hours, 'hours'),
timeStep: timeSteps.hours,
resolveAriaLabel: localeText.hoursClockNumberText,
valueOrReferenceDate,
}),
};
}
Expand Down Expand Up @@ -348,12 +349,14 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi
value: 'am',
label: amLabel,
isSelected: () => !!value && meridiemMode === 'am',
isFocused: () => !!valueOrReferenceDate && meridiemMode === 'am',
ariaLabel: amLabel,
},
{
value: 'pm',
label: pmLabel,
isSelected: () => !!value && meridiemMode === 'pm',
isFocused: () => !!valueOrReferenceDate && meridiemMode === 'pm',
ariaLabel: pmLabel,
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TimeViewWithMeridiem } from '../internals/models';
export interface MultiSectionDigitalClockOption<TValue> {
isDisabled?: (value: TValue) => boolean;
isSelected: (value: TValue) => boolean;
isFocused: (value: TValue) => boolean;
label: string;
value: TValue;
ariaLabel: string;
Expand Down
Loading

0 comments on commit c7682ff

Please sign in to comment.