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

Change month height calculation to be based on number of weeks instead of DOM calculations #1192

Merged
merged 2 commits into from
Jun 5, 2018

Conversation

majapw
Copy link
Collaborator

@majapw majapw commented Jun 1, 2018

Hello friends!

This is a precursor to getting #1106 merged in. I separated it out into its own change because it was fairly involved and I didn't want to pollute the code-change in #1106 with something not entirely related.

Basically, when playing around with the month/year selection I found an issue where if you jumped to a month of a different height (5 week month => 6 week month), the height wouldn't automatically adjust as expected. When I started trying to fix the issue, it became apparent that it wasn't possible to do without jank using the existing paradigms.

Before:

  1. On render, each CalendarMonth component (including the two invisible ones for animation) would measure its own height in the DOM.
  2. Each CalendarMonth would then call a callback prop, setMonthHeight, with this measured value.
  3. The CalendarMonthGrid would add these values to an ordered array of CalendarMonth heights which it would then pass back up to the DayPicker via the setCalendarMonthHeights prop
  4. Once the DayPicker would have all of the heights, it would calculate the expected height based on the max of the visible months and grow the calendar appropriately.
  5. On month transitions, it would use this array of heights to grow or shrink the calendar appropriately while transitioning.

The important thing to note is that the only way it could change the height while transitioning was to use the self-calculations made by the invisible months that were already rendered to the DOM. When we want to transition to an arbitrary month or year, there's guaranteed to be jank in this system because we have to render the month before we know its height. The end result is that if you transition from July 2018 to July 2017, the 6-week month will be cut off before it grows. The other option is to hide the calendar in this interim, but that flash is also very apparent.

SO, this change is designed to amend that.

Now:

  1. On the first render (and any time renderMonth changes), the CalendarMonth measures itself, subtracts out the height of the CalendarDay table (which is deterministic based on number of weeks and the daySize prop), and call the setMonthTitleHeight prop with the value.
  2. The DayPicker receives this value and saves it in this.monthTitleHeight
  3. The DayPicker also maintains an array of the number of weeks in each visible month.
  4. On every month transition, the DayPicker uses the number of weeks in the month coming into view along with the daySize prop and the saved monthTitleHeight to calculate the expected height of the incoming month and adjusts the height appropriately.

I've tested this in both the vertical and the horizontal pickers and it seems pretty effective! Because we can pretty readily calculate the number of weeks in any incoming month, this should open the door to slick month transitions to any month. We also, tbh, could probably remove the invisible months from render until it's time to slide them in. That can wait for another time though.

Note, this does assume a pretty constant month title height, but tbh I think that was an implicit assumption before.

Would love some eyes! @ljharb @gabergg @GoGoCarl

These props that I'm changing are all what I would consider internal props (it would be weird if an outside consumer was relying on them for anything)... but are technically part of the API, so this change could be considered breaking. Idk.

TODO:
-[X] Write tests for getNumberOfCalendarMonthWeeks
-[X] reset month height when renderMonth changes

@majapw majapw added the semver-patch: fixes/refactors/etc Anything that's not major or minor. label Jun 1, 2018
Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is test/utils/getNumberOfCalendarMonthWeeks_spec.js empty?

Yes, I think this is breaking.

if (this.isVertical()) {
const calendarMonthWeeksHeight = this.calendarMonthWeeks[0] * (daySize - 1);
translationValue = monthTitleHeight + calendarMonthWeeksHeight + 1;
}

if (this.isHorizontal()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like it should be an else if

let translationValue = this.isVertical() ? this.calendarMonthHeights[0] : calendarMonthWidth;
const newMonth = currentMonth.clone().subtract(1, 'month');
const numberOfWeeks = getNumberOfCalendarMonthWeeks(newMonth, this.getFirstDayOfWeek());
this.calendarMonthWeeks = [numberOfWeeks].concat(this.calendarMonthWeeks.slice(0, -1));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[numberOfWeeks, ...this.calendarMonthWeeks.slice(0, -1)] ?


let translationValue;
if (this.isVertical()) {
const calendarMonthWeeksHeight = this.calendarMonthWeeks[0] * (daySize - 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this only need the first week?
also, could it do const [firstWeek] = this.calendarMonthWeeks;?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calendarMonthWeeks is an array of the number of weeks in each of the calendarmonths. The vertical picker scrolls forward by the amount of weeks in the first (currently invisible) month.

let translationValue;

if (this.isVertical()) {
const calendarMonthWeeksHeight = this.calendarMonthWeeks[0] * (daySize - 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SO there is a difference between the two. in the one the weeks array has already been updated to the new state; in the other it hasnt'.


if (this.isVertical()) {
const calendarMonthWeeksHeight = this.calendarMonthWeeks[0] * (daySize - 1);
translationValue = -(monthTitleHeight + calendarMonthWeeksHeight + 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this "is vertical" logic is repeated in at least 2 places; can it be shared?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not quite the same

translationValue = -(monthTitleHeight + calendarMonthWeeksHeight + 1);
}

this.calendarMonthWeeks = this.calendarMonthWeeks.slice(1).concat([numberOfWeeks]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const [, ...rest] = this.calendarMonthWeeks; this.calendarMonthWeeks = [...rest, numberOfWeeks];?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay

@ljharb ljharb added semver-major: breaking change A non-backwards-compatible change; anything that breaks code - including adding a peerDep. and removed semver-patch: fixes/refactors/etc Anything that's not major or minor. labels Jun 1, 2018
@coveralls
Copy link

coveralls commented Jun 1, 2018

Coverage Status

Coverage increased (+0.3%) to 85.179% when pulling 0bd08cb on maja-calc-height-based-on-weeks-instead-of-DOM into 2e13fdc on master.

@majapw majapw force-pushed the maja-calc-height-based-on-weeks-instead-of-DOM branch 2 times, most recently from 319a3fa to 6f1da2e Compare June 1, 2018 21:02
Copy link
Collaborator Author

@majapw majapw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PTAL @ljharb

@majapw majapw force-pushed the maja-calc-height-based-on-weeks-instead-of-DOM branch from 6f1da2e to 8dec891 Compare June 1, 2018 21:24
Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

o/

this.calendarMonthGridHeight = Math.max(0, ...visibleCalendarMonthHeights) + MONTH_PADDING;
this.setState({ hasSetHeight: true });
this.calendarMonthWeeks = [];
for (let i = 0; i < numberOfMonths; i += 1) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could tweak this to in the same way to capture months before and after the current month? And then on prev/next click, we could update the neighbor months asynchronously, the way that we might preload images in a carousel? I could be misunderstanding the logic too.


if (e) e.preventDefault();

let translationValue = this.isVertical() ? this.calendarMonthHeights[0] : calendarMonthWidth;
const newMonth = currentMonth.clone().subtract(1, 'month');
const numberOfWeeks = getNumberOfCalendarMonthWeeks(newMonth, this.getFirstDayOfWeek());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mostly think it would be cool to reduce the work done on prev/next click. Wondering if we can pre-calculate and key the months somehow, or even memoize getNumberOfCalendarMonthWeeks or something so that we don't have to recalculate week count and shift calendarMonthWeeks on each transition.

Copy link
Collaborator

@gabergg gabergg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. My only comments are potential optimizations that may or may not make sense and/or matter

Copy link
Contributor

@GoGoCarl GoGoCarl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@majapw Looks great, the approach makes sense, and works as advertised!

Looking forward to #1106, the onMonthChange and onYearChange functions will need to have code added to conditionally adjust the height, as you did with onPrevMonthClick and onNextMonthClick. I was able to verify this behavior by adding this function:

maybeAdjustDayPickerHeight(newMonth) {
    const { daySize } = this.props;
    const { monthTitleHeight } = this.state;

    const numberOfWeeks = getNumberOfCalendarMonthWeeks(newMonth, this.getFirstDayOfWeek());
    this.calendarMonthWeeks = [numberOfWeeks, ...this.calendarMonthWeeks.slice(0, -1)];

    if (this.isHorizontal()) {
      const calendarMonthWeeksHeight = Math.max(0, ...this.calendarMonthWeeks) * (daySize - 1);
      const newMonthHeight = monthTitleHeight + calendarMonthWeeksHeight + 1;
      this.adjustDayPickerHeight(newMonthHeight);
    }
  }

And calling it in onMonthChange and onYearChange just before the setState call, and the heights were adjusted as I'd expect. There may (or may not) need to be more done there, and the transitions are still the same (0.0001), but it seems to work nicely!

For a couple quick checks/verification, I tested with two cases:

  • Jumping from June 2018 to January 2018 to September 2018 -- this used to clip the month height, now it does not
  • Jumping from June 2018 to June 2019 to June 2018 -- sizes up and down correctly!

That said, perhaps that maybeAdjustDayPickerHeight (based on code from onPrevMonthClick) could also be called from onPrevMonthClick and onNextMonthClick? I know one of them is slightly different, but perhaps?

Anyway, all that is for later, this PR as-is LGTM.

@majapw majapw force-pushed the maja-calc-height-based-on-weeks-instead-of-DOM branch from 4605dde to 0bd08cb Compare June 4, 2018 23:57
@majapw
Copy link
Collaborator Author

majapw commented Jun 5, 2018

Sweet! Okay, I deferred some of the heavier cloning logic from onPrevMonthClick/onNextMonthClick to updateStateAfterTransition. @GoGoCarl I think you're right that we'll likely want to abstract out that logic a bit in the month/year change. We can address that in your PR though, I think.

I'm gonna merge this in once everything passes.

@majapw majapw merged commit ef42df2 into master Jun 5, 2018
@majapw majapw deleted the maja-calc-height-based-on-weeks-instead-of-DOM branch June 5, 2018 01:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver-major: breaking change A non-backwards-compatible change; anything that breaks code - including adding a peerDep.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants