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

[DateRangeCalendar] Support arrow navigation with multiple months rendered #16363

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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 @@ -22,12 +22,10 @@ const CustomCalendarHeaderRoot = styled('div')({
function CustomCalendarHeader(props) {
const { currentMonth, onMonthChange } = props;

const selectNextMonth = () => onMonthChange(currentMonth.add(1, 'month'), 'left');
const selectNextYear = () => onMonthChange(currentMonth.add(1, 'year'), 'left');
const selectPreviousMonth = () =>
onMonthChange(currentMonth.subtract(1, 'month'), 'right');
const selectPreviousYear = () =>
onMonthChange(currentMonth.subtract(1, 'year'), 'right');
const selectNextMonth = () => onMonthChange(currentMonth.add(1, 'month'));
const selectNextYear = () => onMonthChange(currentMonth.add(1, 'year'));
const selectPreviousMonth = () => onMonthChange(currentMonth.subtract(1, 'month'));
const selectPreviousYear = () => onMonthChange(currentMonth.subtract(1, 'year'));

return (
<CustomCalendarHeaderRoot>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@ const CustomCalendarHeaderRoot = styled('div')({
function CustomCalendarHeader(props: PickersCalendarHeaderProps) {
const { currentMonth, onMonthChange } = props;

const selectNextMonth = () => onMonthChange(currentMonth.add(1, 'month'), 'left');
const selectNextYear = () => onMonthChange(currentMonth.add(1, 'year'), 'left');
const selectPreviousMonth = () =>
onMonthChange(currentMonth.subtract(1, 'month'), 'right');
const selectPreviousYear = () =>
onMonthChange(currentMonth.subtract(1, 'year'), 'right');
const selectNextMonth = () => onMonthChange(currentMonth.add(1, 'month'));
const selectNextYear = () => onMonthChange(currentMonth.add(1, 'year'));
const selectPreviousMonth = () => onMonthChange(currentMonth.subtract(1, 'month'));
const selectPreviousYear = () => onMonthChange(currentMonth.subtract(1, 'year'));

return (
<CustomCalendarHeaderRoot>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ const CustomCalendarHeaderRoot = styled('div')({
function CustomCalendarHeader(props) {
const { currentMonth, onMonthChange, month, calendars, monthIndex } = props;

const selectNextMonth = () =>
onMonthChange(currentMonth.add(calendars, 'month'), 'left');
const selectNextMonth = () => onMonthChange(currentMonth.add(calendars, 'month'));
const selectPreviousMonth = () =>
onMonthChange(currentMonth.subtract(calendars, 'month'), 'right');
onMonthChange(currentMonth.subtract(calendars, 'month'));

return (
<CustomCalendarHeaderRoot>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ const CustomCalendarHeaderRoot = styled('div')({
function CustomCalendarHeader(props: PickersRangeCalendarHeaderProps) {
const { currentMonth, onMonthChange, month, calendars, monthIndex } = props;

const selectNextMonth = () =>
onMonthChange(currentMonth.add(calendars, 'month'), 'left');
const selectNextMonth = () => onMonthChange(currentMonth.add(calendars, 'month'));
const selectPreviousMonth = () =>
onMonthChange(currentMonth.subtract(calendars, 'month'), 'right');
onMonthChange(currentMonth.subtract(calendars, 'month'));

return (
<CustomCalendarHeaderRoot>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
useDefaultDates,
useUtils,
PickerSelectionState,
useNow,
DEFAULT_DESKTOP_MODE_MEDIA_QUERY,
useControlledValueWithTimezone,
useViews,
Expand Down Expand Up @@ -197,6 +196,8 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar(
loading,
renderLoading,
disableHighlightToday,
focusedView: focusedViewProp,
onFocusedViewChange,
readOnly,
disabled,
showDaysOutsideCurrentMonth,
Expand Down Expand Up @@ -230,17 +231,18 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar(
valueManager: rangeValueManager,
});

const { setValueAndGoToNextView, view } = useViews({
const { view, setFocusedView, focusedView, setValueAndGoToNextView } = useViews({
view: inView,
views,
openTo,
onChange: handleValueChange,
onViewChange,
autoFocus,
focusedView: focusedViewProp,
onFocusedViewChange,
});

const utils = useUtils();
const now = useNow(timezone);
const id = useId();

const { rangePosition, setRangePosition } = useRangePosition({
Expand Down Expand Up @@ -342,24 +344,39 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar(
shouldDisableDate(dayToTest, draggingDatePosition || rangePosition);
}, [shouldDisableDate, rangePosition, draggingDatePosition]);

const {
calendarState,
changeFocusedDay,
changeMonth,
handleChangeMonth,
onMonthSwitchingAnimationEnd,
} = useCalendarState({
const { calendarState, setVisibleDate, onMonthSwitchingAnimationEnd } = useCalendarState({
value: value[0] || value[1],
referenceDate,
disableFuture,
disablePast,
disableSwitchToMonthOnDayFocus: true,
maxDate,
minDate,
onMonthChange,
reduceAnimations,
shouldDisableDate: wrappedShouldDisableDate,
timezone,
getCurrentMonthFromVisibleDate: (visibleDate, prevMonth) => {
const firstVisibleMonth = utils.addMonths(prevMonth, 1 - currentMonthCalendarPosition);
const lastVisibleMonth = utils.endOfMonth(utils.addMonths(firstVisibleMonth, calendars - 1));

// The new focused day is inside the visible calendars,
// Do not change the current month
if (utils.isWithinRange(visibleDate, [firstVisibleMonth, lastVisibleMonth])) {
return prevMonth;
}

// The new focused day is after the last visible month,
// Move the current month so that the new focused day is inside the first visible month
if (utils.isAfter(visibleDate, lastVisibleMonth)) {
return utils.startOfMonth(utils.addMonths(visibleDate, currentMonthCalendarPosition - 1));
}

// The new focused day is before the first visible month,
// Move the current month so that the new focused day is inside the last visible month
return utils.startOfMonth(
utils.addMonths(visibleDate, currentMonthCalendarPosition - calendars),
);
},
});

const CalendarHeader = slots?.calendarHeader ?? PickersRangeCalendarHeader;
Expand All @@ -372,7 +389,7 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar(
views: ['day'],
view: 'day',
currentMonth: calendarState.currentMonth,
onMonthChange: (newMonth, direction) => handleChangeMonth({ newMonth, direction }),
onMonthChange: (month) => setVisibleDate({ target: month, reason: 'header-navigation' }),
minDate,
maxDate,
disabled,
Expand All @@ -386,6 +403,7 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar(
ownerState,
});

// TODO: Move this logic inside the render instead of using an effect
const prevValue = React.useRef<PickerRangeValue | null>(null);
React.useEffect(() => {
const date = rangePosition === 'start' ? value[0] : value[1];
Expand Down Expand Up @@ -417,7 +435,7 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar(
: // If need to focus end, scroll to the state when "end" is displaying in the last calendar
utils.addMonths(date, -displayingMonthRange);

changeMonth(newMonth);
setVisibleDate({ target: newMonth, reason: 'controlled-value-change' });
}
}, [rangePosition, value]); // eslint-disable-line

Expand Down Expand Up @@ -533,27 +551,21 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar(
return Array.from({ length: calendars }).map((_, index) => utils.addMonths(firstMonth, index));
}, [utils, calendarState.currentMonth, calendars, currentMonthCalendarPosition]);

const focusedMonth = React.useMemo(() => {
if (!autoFocus) {
return null;
}

// The focus priority of the "day" view is as follows:
// 1. Month of the `start` date
// 2. Month of the `end` date
// 3. Month of the current date
// 4. First visible month
const hasFocus = focusedView !== null;

if (value[0] != null) {
return visibleMonths.find((month) => utils.isSameMonth(month, value[0]!));
const prevOpenViewRef = React.useRef(view);
React.useEffect(() => {
// If the view change and the focus was on the previous view
// Then we update the focus.
if (prevOpenViewRef.current === view) {
return;
}

if (value[1] != null) {
return visibleMonths.find((month) => utils.isSameMonth(month, value[1]!));
if (focusedView === prevOpenViewRef.current) {
setFocusedView(view, true);
}

return visibleMonths.find((month) => utils.isSameMonth(month, now)) ?? visibleMonths[0];
}, [utils, value, visibleMonths, autoFocus, now]);
prevOpenViewRef.current = view;
}, [focusedView, setFocusedView, view]);

return (
<DateRangeCalendarRoot
Expand Down Expand Up @@ -581,20 +593,23 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar(
{...baseDateValidationProps}
{...commonViewProps}
onMonthSwitchingAnimationEnd={onMonthSwitchingAnimationEnd}
onFocusedDayChange={changeFocusedDay}
onFocusedDayChange={(focusedDate) =>
setVisibleDate({ target: focusedDate, reason: 'cell-interaction' })
}
reduceAnimations={reduceAnimations}
selectedDays={value}
onSelectedDaysChange={handleSelectedDayChange}
currentMonth={month}
TransitionProps={CalendarTransitionProps}
shouldDisableDate={wrappedShouldDisableDate}
hasFocus={hasFocus}
onFocusedViewChange={(isViewFocused) => setFocusedView('day', isViewFocused)}
showDaysOutsideCurrentMonth={calendars === 1 && showDaysOutsideCurrentMonth}
dayOfWeekFormatter={dayOfWeekFormatter}
loading={loading}
renderLoading={renderLoading}
slots={slotsForDayCalendar}
slotProps={slotPropsForDayCalendar}
autoFocus={month === focusedMonth}
fixedWeekNumber={fixedWeekNumber}
displayWeekNumber={displayWeekNumber}
timezone={timezone}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ const PickersRangeCalendarHeader = React.forwardRef(function PickersRangeCalenda
return <PickersCalendarHeader {...other} labelId={labelId} ref={ref} />;
}

const selectNextMonth = () => onMonthChange(utils.addMonths(currentMonth, 1), 'left');
const selectNextMonth = () => onMonthChange(utils.addMonths(currentMonth, 1));

const selectPreviousMonth = () => onMonthChange(utils.addMonths(currentMonth, -1), 'right');
const selectPreviousMonth = () => onMonthChange(utils.addMonths(currentMonth, -1));

return (
<PickersRangeCalendarHeaderContentMultipleCalendars
Expand Down
35 changes: 19 additions & 16 deletions packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export const DateCalendar = React.forwardRef(function DateCalendar(
minDate,
maxDate,
disableHighlightToday,
focusedView: inFocusedView,
focusedView: focusedViewProp,
onFocusedViewChange,
showDaysOutsideCurrentMonth,
fixedWeekNumber,
Expand Down Expand Up @@ -169,16 +169,14 @@ export const DateCalendar = React.forwardRef(function DateCalendar(
onChange: handleValueChange,
onViewChange,
autoFocus,
focusedView: inFocusedView,
focusedView: focusedViewProp,
onFocusedViewChange,
});

const {
referenceDate,
calendarState,
changeFocusedDay,
changeMonth,
handleChangeMonth,
setVisibleDate,
isDateDisabled,
onMonthSwitchingAnimationEnd,
} = useCalendarState({
Expand All @@ -192,6 +190,13 @@ export const DateCalendar = React.forwardRef(function DateCalendar(
disablePast,
disableFuture,
timezone,
getCurrentMonthFromVisibleDate: (visibleDate, prevMonth) => {
if (utils.isSameMonth(visibleDate, prevMonth)) {
return prevMonth;
}

return utils.startOfMonth(visibleDate);
},
});

// When disabled, limit the view to the selected date
Expand All @@ -210,7 +215,7 @@ export const DateCalendar = React.forwardRef(function DateCalendar(
view,
currentMonth: calendarState.currentMonth,
onViewChange: setView,
onMonthChange: (newMonth, direction) => handleChangeMonth({ newMonth, direction }),
onMonthChange: (month) => setVisibleDate({ target: month, reason: 'header-navigation' }),
minDate: minDateWithDisabled,
maxDate: maxDateWithDisabled,
disabled,
Expand Down Expand Up @@ -242,13 +247,11 @@ export const DateCalendar = React.forwardRef(function DateCalendar(

if (closestEnabledDate) {
setValueAndGoToNextView(closestEnabledDate, 'finish');
onMonthChange?.(startOfMonth);
setVisibleDate({ target: closestEnabledDate, reason: 'cell-interaction' });
} else {
goToNextView();
changeMonth(startOfMonth);
setVisibleDate({ target: startOfMonth, reason: 'cell-interaction' });
}

changeFocusedDay(closestEnabledDate, true);
});

const handleDateYearChange = useEventCallback((newDate: PickerValidDate) => {
Expand All @@ -270,13 +273,11 @@ export const DateCalendar = React.forwardRef(function DateCalendar(

if (closestEnabledDate) {
setValueAndGoToNextView(closestEnabledDate, 'finish');
onYearChange?.(closestEnabledDate);
setVisibleDate({ target: closestEnabledDate, reason: 'cell-interaction' });
} else {
goToNextView();
changeMonth(startOfYear);
setVisibleDate({ target: startOfYear, reason: 'cell-interaction' });
}

changeFocusedDay(closestEnabledDate, true);
});

const handleSelectedDayChange = useEventCallback((day: PickerValidDate | null) => {
Expand All @@ -294,7 +295,7 @@ export const DateCalendar = React.forwardRef(function DateCalendar(

React.useEffect(() => {
if (utils.isValid(value)) {
changeMonth(value);
setVisibleDate({ target: value, reason: 'controlled-value-change' });
}
}, [value]); // eslint-disable-line

Expand Down Expand Up @@ -384,7 +385,9 @@ export const DateCalendar = React.forwardRef(function DateCalendar(
{...baseDateValidationProps}
{...commonViewProps}
onMonthSwitchingAnimationEnd={onMonthSwitchingAnimationEnd}
onFocusedDayChange={changeFocusedDay}
onFocusedDayChange={(focusedDate) =>
setVisibleDate({ target: focusedDate, reason: 'cell-interaction' })
}
reduceAnimations={reduceAnimations}
selectedDays={selectedDays}
onSelectedDaysChange={handleSelectedDayChange}
Expand Down
Loading