Skip to content

feat: month animation#2684

Merged
rodgobbi merged 39 commits into
gpbl:mainfrom
rodgobbi:new/87-month-animation
Mar 1, 2025
Merged

feat: month animation#2684
rodgobbi merged 39 commits into
gpbl:mainfrom
rodgobbi:new/87-month-animation

Conversation

@rodgobbi

@rodgobbi rodgobbi commented Feb 16, 2025

Copy link
Copy Markdown
Collaborator

Idea from the board:
DayPicker Plans (view)

Description

Add a feature to animate the calendar changing the displayed month. The feature can be enabled by setting the prop animate to true.

The core idea in the approach of this implementation is to imperatively manipulate the DOM when the displayed months change, because doing it with only React APIs is very convoluted and has bad performance.
All the DOM manipulation happens inside a useLayoutEffect to set up the animation right before the browser renders.

There is a new set of CSS classes that are imperatively applied to the DOM to trigger the animations that are based on the calendar changing to a month that is after the previous month or before it because depending on this condition, the calendar weeks should animate left or right, and is inverted when the direction is rtl.

The animation is disabled when navigating the calendar with the keyboard arrows because focusing on the month being animated can cause issues with the animation. Animation can be worse for a11y, therefore disabling for keyboard navigation seems better.

Demo

Single.mov
Rtl.mov
Multiple.mov

Comment thread examples-app/src/App.css Outdated
Comment thread src/components/Root.tsx
Comment thread src/DayPicker.tsx Outdated
Comment thread src/DayPicker.tsx Outdated
Comment thread src/DayPicker.tsx Outdated
Comment thread src/DayPicker.tsx Outdated
Comment thread src/DayPicker.tsx Outdated

@gpbl gpbl left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This looks so great, I love how you nailed it!

Next steps to me would be:

  • Move the useLayoutEffect into a useAnimation hook.
  • Add a test for the new useAnimation.
  • Improve readability with some refactoring.

Comment thread src/UI.ts Outdated
Comment thread src/UI.ts Outdated
Comment thread src/style.css Outdated
Comment thread src/types/props.ts Outdated
Comment thread src/DayPicker.tsx Outdated
@rodgobbi rodgobbi changed the title feat: month animation prototype feat: month animation Feb 20, 2025
Comment thread src/UI.ts Outdated
/** CSS classes used for animating months and captions. */
export enum Animation {
/** Applied to the entering month when it is after the exiting month. */
animation_enter_month_weeks_is_after = "animation_enter_month_weeks_is_after",

@rodgobbi rodgobbi Feb 20, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@gpbl FYI, I changed the name of these classes to month_weeks to follow the same patter of the month_caption and be more explicit that it's being applied to the Weeks component.
Lemme know what you think.

@gpbl gpbl Feb 26, 2025

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I love everything but the verbosity here :) What about:

export enum Animation {
  weeks_next_enter = "weeks_next_enter",
  weeks_next_exit = "weeks_next_exit",
  weeks_prev_enter = "weeks_prev_enter",
  weeks_prev_exit = "weeks_prev_exit",
  caption_next_enter = "caption_next_enter",
  caption_next_exit = "caption_next_exit",
  caption_prev_enter = "caption_prev_enter",
  caption_prev_exit = "caption_prev_exit"
}

@rodgobbi rodgobbi marked this pull request as ready for review February 20, 2025 20:40
Comment thread src/style.module.css
@rodgobbi rodgobbi requested a review from gpbl February 20, 2025 20:42

@gpbl gpbl left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Fanstastic work @rodgobbi !

The work is done here and we may want to polish a bit the code before merging.

My suggestions:

  • useAnimation: Remove the return value and clean up the arguments.
  • Shorten CSS class and keyframe names to avoid cumbersome customization.
  • Rename animation-related data attributes like data-month-containerdata-animated-month.

If you prefer, I can propose my suggested changes in a separate branch.

Comment thread src/style.css Outdated
Comment on lines +327 to +334
.rdp-animation_enter_month_weeks_is_after {
animation: rdp-animation_slide_in_right_keyframes var(--rdp-animation_duration) var(--rdp-animation_timing) forwards;
}

.rdp-animation_exit_month_weeks_is_before {
animation: rdp-animation_slide_out_left_keyframes var(--rdp-animation_duration) var(--rdp-animation_timing) forwards;
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think a shorter names will help "grouping" these classes and ease up further customization:

Suggested change
.rdp-animation_enter_month_weeks_is_after {
animation: rdp-animation_slide_in_right_keyframes var(--rdp-animation_duration) var(--rdp-animation_timing) forwards;
}
.rdp-animation_exit_month_weeks_is_before {
animation: rdp-animation_slide_out_left_keyframes var(--rdp-animation_duration) var(--rdp-animation_timing) forwards;
}
.rdp-weeks_next_in {
animation: rdp-slide_in_next var(--rdp-animation_duration) var(--rdp-animation_timing) forwards;
}
.rdp-weeks_prev_out {
animation: rdp-slide_out_prev var(--rdp-animation_duration) var(--rdp-animation_timing) forwards;
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Indeed sounds better being shorter.
But a couple of points to discuss about the new format you suggested:

  1. What about adopting the same terms already established by other libs like react-transtion-group, instead of in and out, we stick with enter and exit. You suggested this naming in the previous comments, and I think it's great because we reuse the same pattern already used by other libs. We could check if there are other commonly used libs that use another naming pattern.

  2. About the naming of the keyfram animation, I think it's not ideal to use prev or next, the keyframe animation itself represents sliding in from left or right, or sliding out to the left or right, and it doesn't change, but with the CSS classes and rtl attribute we use different keyframes based on the case, the keyframe will still represent left or right transitions, but the class represents how to animate the elements based on the month entering the DOM being next or previous (after/before), and the text direction preference.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@rodgobbi, you’re absolutely right. Let’s maintain the usage of “enter” and “exit” instead of “in” and “out,” and “left” and “right” for the keyframe names.

Comment thread src/style.css Outdated
cursor: pointer;
}

@keyframes rdp-animation_slide_in_left_keyframes {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think we should shorten the keyframes names as:

Suggested change
@keyframes rdp-animation_slide_in_left_keyframes {
@keyframes rdp-slide_in_before {

Reasoning - it's clear it belongs to animation and these are keyframes (no chance of "name collisions" to me).

(not sure for "left/right" yet)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Discussed this in point 2 on the #2684 (comment)

Comment thread src/DayPicker.tsx Outdated
Comment on lines +260 to +267
const { rootElRef } = useAnimation(
props,
classNames,
months,
focused,
dateLib
);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Love this! It sounds strange to me that we need to return something here. Couldn't we initialize the root ref into the component instead?

I also propose a different arguments API:

Suggested change
const { rootElRef } = useAnimation(
props,
classNames,
months,
focused,
dateLib
);
const rootElRef = useRef();
useAnimation(rootElRef, enabled, { classNames, months, focused, dateLib });

Looks more declarative to me.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense 👍
the initial idea was to isolate the rootElRef in the hook, but for me it indeed makes sense to pass it as an argument.

Comment thread src/DayPicker.tsx Outdated

return (
<components.Month
data-month-container={props.animate ? "true" : undefined}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggested change
data-month-container={props.animate ? "true" : undefined}
data-animated-month={props.animate ? "true" : undefined}

Here and in the other data attributes, I would always use the animated- prefix instead of container because these data attributes are only valid in an animated calendar.

I like that we do not render them when not needed 🔝

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

good idea 👍
it makes more clear it's for the animation

Comment thread src/useAnimation.test.tsx
Comment thread src/useAnimation.test.tsx
Comment on lines +27 to +29
describe("animate prop is falsy", () => {
it("should not change the default props", () => {
render(<DayPicker />);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I'd like to call "data attributes" the data- things added to the elements:

Suggested change
describe("animate prop is falsy", () => {
it("should not change the default props", () => {
render(<DayPicker />);
describe("animate prop is not set", () => {
it("should not render any data-animated element", () => {
render(<DayPicker />);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

make sense, I'll rephrase a bit because not render any data-animated element looks like the element will not be rendered at all.

Comment thread src/useAnimation.test.tsx Outdated
Comment on lines +39 to +41
it("should add data attributes if animate is true", () => {
render(<DayPicker animate={true} numberOfMonths={2} />);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggested change
it("should add data attributes if animate is true", () => {
render(<DayPicker animate={true} numberOfMonths={2} />);
it("should render data-animated elements", () => {
render(<DayPicker animate={true} numberOfMonths={2} />);

Comment thread src/useAnimation.ts
Comment thread src/useAnimation.ts Outdated
Comment thread src/UI.ts Outdated
/** CSS classes used for animating months and captions. */
export enum Animation {
/** Applied to the entering month when it is after the exiting month. */
animation_enter_month_weeks_is_after = "animation_enter_month_weeks_is_after",

@gpbl gpbl Feb 26, 2025

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I love everything but the verbosity here :) What about:

export enum Animation {
  weeks_next_enter = "weeks_next_enter",
  weeks_next_exit = "weeks_next_exit",
  weeks_prev_enter = "weeks_prev_enter",
  weeks_prev_exit = "weeks_prev_exit",
  caption_next_enter = "caption_next_enter",
  caption_next_exit = "caption_next_exit",
  caption_prev_enter = "caption_prev_enter",
  caption_prev_exit = "caption_prev_exit"
}

@rodgobbi rodgobbi requested a review from gpbl February 28, 2025 19:53

@gpbl gpbl left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Looking great! Thanks @rodgobbi for your patience addressing my reviews.

@rodgobbi rodgobbi merged commit e05f51b into gpbl:main Mar 1, 2025
@gpbl gpbl linked an issue Mar 8, 2025 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Animation when changing months

2 participants