Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[pickers] Refine referenceDate behavior in views #10863

Merged
merged 12 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -204,14 +204,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 @@ -283,6 +286,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 @@ -291,16 +298,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 @@ -312,6 +320,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 @@ -135,13 +135,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 @@ -304,6 +304,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 @@ -350,12 +351,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 @@ -13,6 +13,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