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

[YearPicker] Scroll to the current year even with autoFocus=false #6089

Merged
merged 13 commits into from
Sep 14, 2022
25 changes: 10 additions & 15 deletions .github/ISSUE_TEMPLATE/5.rfc.yml
Original file line number Diff line number Diff line change
@@ -1,47 +1,42 @@
name: RFC 💬
description: Request for comments for your proposal.
title: "[RFC] "
title: '[RFC] '
labels: ['RFC']
body:
- type: markdown
attributes:
value: |
Please provide a searchable summary of the RFC in the title above ⬆️.
Please provide a searchable summary of the RFC in the title above. ⬆️

Thanks for contributing by creating an RFC! ❤️
- type: textarea
attributes:
label: What's the problem? 🤔
description: A short paragraph or bullet list that quickly explains what you're trying to do, what value do we expect to get or any other relevant information to understand motivation behind this RFC
description: Write a short paragraph or bulleted list to briefly explain what you're trying to do, what outcomes you're aiming for, and any other relevant details to help us understand the motivation behind this RFC.

- type: textarea
attributes:
label: What are the requirements? ❓
description: Provide a list of requirements that should be met by the accepted proposal
description: Provide a list of requirements that should be met by the accepted proposal.

- type: textarea
attributes:
label: What are our options? 💡
description: |
Have you considered all options that we can achieve same/similar thing? It doesn't have to be explored in same detail as the main proposal, but having alternative options that you can present can help strengthen main proposal.

Consider:
- using diagrams to help illustrate your ideas
- including code examples if you're proposing an interface or system contract
- linking to project briefs or wireframes that are relevant
Have you considered alternative options for achieving your desired outcome? It's not necessary to go into too much detail here, but it can help strengthen your main proposal.

- type: textarea
attributes:
label: Proposed solution 🟢
description: |
This is the core of RFC, and its purpose is to clearly explain reasoning for the proposed solution, why it's better compared to the other options or why should it be chosen instead.
This is the core of the RFC. Please clearly explain the reasoning behind your proposed solution, including why it would be preferred over possible alternatives.

Consider:
- using diagrams to help illustrate your ideas
- including code examples if you're proposing an interface or system contract
- linking to project briefs or wireframes that are relevant
- linking to relevant project briefs or wireframes

- type: textarea
attributes:
label: Relevant resources/benchmarks 🔗
label: Resources and benchmarks 🔗
description: Attach any issues, PRs, links, documents, etc… that might be relevant to the RFC
30 changes: 30 additions & 0 deletions packages/x-date-pickers/src/CalendarPicker/CalendarPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
createPickerRenderer,
} from '../../../../test/utils/pickers-utils';

const isJSDOM = /jsdom/.test(window.navigator.userAgent);

describe('<CalendarPicker />', () => {
const { render, clock } = createPickerRenderer({ clock: 'fake' });

Expand Down Expand Up @@ -410,5 +412,33 @@ describe('<CalendarPicker />', () => {
expect(onChange.callCount).to.equal(0);
expect(screen.getByMuiTest('calendar-month-and-year-text')).to.have.text('January 2022');
});

it('should scroll to show the selected year', function test() {
if (isJSDOM) {
this.skip(); // Needs layout
}
render(
<CalendarPicker
date={adapterToUse.date(new Date(2019, 3, 29))}
onChange={() => {}}
views={['year']}
openTo="year"
/>,
);

// const virtualScroller = document.querySelector('.MuiDataGrid-virtualScroller')!;
//
const rootElement = document.querySelector('.MuiCalendarPicker-root')!;
const selectedButton = document.querySelector('.Mui-selected')!;

expect(rootElement).not.to.equal(null);
expect(selectedButton).not.to.equal(null);

const parentBoundingBox = rootElement.getBoundingClientRect();
const buttonBoundingBox = selectedButton.getBoundingClientRect();

expect(parentBoundingBox.top).not.to.greaterThan(buttonBoundingBox.top);
expect(parentBoundingBox.bottom).not.to.lessThan(buttonBoundingBox.bottom);
});
});
});
160 changes: 99 additions & 61 deletions packages/x-date-pickers/src/CalendarPicker/CalendarPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,15 @@ const CalendarPickerViewTransitionContainer = styled(PickersFadeTransitionGroup,
name: 'MuiCalendarPicker',
slot: 'ViewTransitionContainer',
overridesResolver: (props, styles) => styles.viewTransitionContainer,
})<{ ownerState: CalendarPickerProps<any> }>({
})<{ ownerState: CalendarPickerProps<any> }>({});

const CalendarPickerViewContainer = styled('div', {
name: 'MuiCalendarPicker',
slot: 'ViewContainer',
overridesResolver: (props, styles) => styles.viewTransitionContainer,
})({
overflowY: 'auto',
position: 'relative',
});

type CalendarPickerComponent = (<TDate>(
Expand Down Expand Up @@ -456,6 +463,36 @@ export const CalendarPicker = React.forwardRef(function CalendarPicker<TDate>(
},
);

const scrollerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (autoFocus || openView !== 'year' || scrollerRef.current === null) {
return;
}

const selectedButton = scrollerRef.current.getElementsByClassName(
'Mui-selected',
)[0] as HTMLElement;
if (!selectedButton) {
return;
}

// Taken from useScroll in x-data-grid, but vertically centered
const offsetHeight = selectedButton.offsetHeight;
const offsetTop = selectedButton.offsetTop;

const clientHeight = scrollerRef.current.clientHeight;
const scrollTop = scrollerRef.current.scrollTop;

const elementBottom = offsetTop + offsetHeight;

if (offsetHeight > clientHeight || offsetTop < scrollTop) {
// Button already visible
return;
}
// return
scrollerRef.current.scrollTop = elementBottom - clientHeight / 2 - offsetHeight / 2;
}, [autoFocus, openView]);

return (
<CalendarPickerRoot ref={ref} className={clsx(classes.root, className)} ownerState={ownerState}>
<PickersCalendarHeader
Expand All @@ -477,66 +514,67 @@ export const CalendarPicker = React.forwardRef(function CalendarPicker<TDate>(
leftArrowButtonText={leftArrowButtonText}
rightArrowButtonText={rightArrowButtonText}
/>
<CalendarPickerViewTransitionContainer
reduceAnimations={reduceAnimations}
className={classes.viewTransitionContainer}
transKey={openView}
ownerState={ownerState}
>
<div>
{openView === 'year' && (
<YearPicker
{...baseDateValidationProps}
{...commonViewProps}
autoFocus={autoFocus}
date={date}
onChange={handleDateYearChange}
shouldDisableYear={shouldDisableYear}
hasFocus={hasFocus}
onFocusedViewChange={handleFocusedViewChange('year')}
/>
)}

{openView === 'month' && (
<MonthPicker
{...baseDateValidationProps}
{...commonViewProps}
autoFocus={autoFocus}
hasFocus={hasFocus}
className={className}
date={date}
onChange={handleDateMonthChange}
shouldDisableMonth={shouldDisableMonth}
onFocusedViewChange={handleFocusedViewChange('month')}
/>
)}

{openView === 'day' && (
<DayPicker
{...calendarState}
{...baseDateValidationProps}
{...commonViewProps}
autoFocus={autoFocus}
onMonthSwitchingAnimationEnd={onMonthSwitchingAnimationEnd}
onFocusedDayChange={changeFocusedDay}
reduceAnimations={reduceAnimations}
selectedDays={[date]}
onSelectedDaysChange={onSelectedDayChange}
shouldDisableDate={shouldDisableDate}
shouldDisableMonth={shouldDisableMonth}
shouldDisableYear={shouldDisableYear}
hasFocus={hasFocus}
onFocusedViewChange={handleFocusedViewChange('day')}
gridLabelId={gridLabelId}
showDaysOutsideCurrentMonth={showDaysOutsideCurrentMonth}
dayOfWeekFormatter={dayOfWeekFormatter}
renderDay={renderDay}
loading={loading}
renderLoading={renderLoading}
/>
)}
</div>
</CalendarPickerViewTransitionContainer>
<CalendarPickerViewContainer ref={scrollerRef}>
<CalendarPickerViewTransitionContainer
reduceAnimations={reduceAnimations}
className={classes.viewTransitionContainer}
transKey={openView}
ownerState={ownerState}
>
<div>
{openView === 'year' && (
<YearPicker
{...baseDateValidationProps}
{...commonViewProps}
autoFocus={autoFocus}
date={date}
onChange={handleDateYearChange}
shouldDisableYear={shouldDisableYear}
hasFocus={hasFocus}
onFocusedViewChange={handleFocusedViewChange('year')}
/>
)}
{openView === 'month' && (
<MonthPicker
{...baseDateValidationProps}
{...commonViewProps}
autoFocus={autoFocus}
hasFocus={hasFocus}
className={className}
date={date}
onChange={handleDateMonthChange}
shouldDisableMonth={shouldDisableMonth}
onFocusedViewChange={handleFocusedViewChange('month')}
/>
)}

{openView === 'day' && (
<DayPicker
{...calendarState}
{...baseDateValidationProps}
{...commonViewProps}
autoFocus={autoFocus}
onMonthSwitchingAnimationEnd={onMonthSwitchingAnimationEnd}
onFocusedDayChange={changeFocusedDay}
reduceAnimations={reduceAnimations}
selectedDays={[date]}
onSelectedDaysChange={onSelectedDayChange}
shouldDisableDate={shouldDisableDate}
shouldDisableMonth={shouldDisableMonth}
shouldDisableYear={shouldDisableYear}
hasFocus={hasFocus}
onFocusedViewChange={handleFocusedViewChange('day')}
gridLabelId={gridLabelId}
showDaysOutsideCurrentMonth={showDaysOutsideCurrentMonth}
dayOfWeekFormatter={dayOfWeekFormatter}
renderDay={renderDay}
loading={loading}
renderLoading={renderLoading}
/>
)}
</div>
</CalendarPickerViewTransitionContainer>
</CalendarPickerViewContainer>
</CalendarPickerRoot>
);
}) as CalendarPickerComponent;
Expand Down
2 changes: 1 addition & 1 deletion packages/x-date-pickers/src/YearPicker/PickersYear.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ const PickersYearRaw = (props: PickersYearProps) => {

const classes = useUtilityClasses(ownerState);

// TODO: Can we just forward this to the button?
// We can't forward the `autoFocus` to the button because it is a native button, not a MUI Button
React.useEffect(() => {
if (autoFocus) {
// `ref.current` being `null` would be a bug in MUI.
Expand Down
1 change: 1 addition & 0 deletions packages/x-date-pickers/src/YearPicker/YearPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export const YearPicker = React.forwardRef(function YearPicker<TDate>(

return utils.getYear(now);
}, [now, date, utils, disableHighlightToday]);

const [focusedYear, setFocusedYear] = React.useState(() => selectedYear || todayYear);

const [internalHasFocus, setInternalHasFocus] = useControlled<boolean>({
Expand Down