Skip to content

Commit

Permalink
Navigator: use CSS animations instead of framer-motion (#56909)
Browse files Browse the repository at this point in the history
* Move navigator provider styles to separate file

* Move navigator screen styles to separate file, use CSS animations instead of framer motion

* Remove unused import

* Spacing

* Use standard ease-in-out easing function

* Remove stale comments

* Remove animation-specific tests (as they can't be tested in jsdom)

* CHANGELOG

* Add comment

* Avoid running the `css` function when unnecessary
  • Loading branch information
ciampo authored and artemiomorales committed Jan 4, 2024
1 parent f76f371 commit db81497
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 161 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Enhancements

- `Navigator`: use vanilla CSS animations instead of `framer-motion` ([#56909](https://github.com/WordPress/gutenberg/pull/56909)).
- `FormToggle`: fix sass deprecation warning ([#56672](https://github.com/WordPress/gutenberg/pull/56672)).
- `QueryControls`: Add opt-in prop for 40px default size ([#56576](https://github.com/WordPress/gutenberg/pull/56576)).
- `CheckboxControl`: Add option to not render label ([#56158](https://github.com/WordPress/gutenberg/pull/56158)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* External dependencies
*/
import type { ForwardedRef } from 'react';
import { css } from '@emotion/react';

/**
* WordPress dependencies
Expand All @@ -23,15 +22,16 @@ import isShallowEqual from '@wordpress/is-shallow-equal';
import type { WordPressComponentProps } from '../../context';
import { contextConnect, useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
import { patternMatch, findParent } from '../utils/router';
import { View } from '../../view';
import { NavigatorContext } from '../context';
import * as styles from '../styles';
import type {
NavigatorProviderProps,
NavigatorLocation,
NavigatorContext as NavigatorContextType,
Screen,
} from '../types';
import { patternMatch, findParent } from '../utils/router';

type MatchedPath = ReturnType< typeof patternMatch >;
type ScreenAction = { type: string; screen: Screen };
Expand Down Expand Up @@ -248,8 +248,7 @@ function UnconnectedNavigatorProvider(

const cx = useCx();
const classes = useMemo(
// Prevents horizontal overflow while animating screen transitions.
() => cx( css( { overflowX: 'hidden' } ), className ),
() => cx( styles.navigatorProviderWrapper, className ),
[ className, cx ]
);

Expand Down
108 changes: 15 additions & 93 deletions packages/components/src/navigator/navigator-screen/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
* External dependencies
*/
import type { ForwardedRef } from 'react';
// eslint-disable-next-line no-restricted-imports
import type { MotionProps } from 'framer-motion';
// eslint-disable-next-line no-restricted-imports
import { motion } from 'framer-motion';
import { css } from '@emotion/react';

/**
* WordPress dependencies
Expand All @@ -19,8 +14,8 @@ import {
useRef,
useId,
} from '@wordpress/element';
import { useReducedMotion, useMergeRefs } from '@wordpress/compose';
import { isRTL } from '@wordpress/i18n';
import { useMergeRefs } from '@wordpress/compose';
import { isRTL as isRTLFn } from '@wordpress/i18n';
import { escapeAttribute } from '@wordpress/escape-html';

/**
Expand All @@ -31,22 +26,11 @@ import { contextConnect, useContextSystem } from '../../context';
import { useCx } from '../../utils/hooks/use-cx';
import { View } from '../../view';
import { NavigatorContext } from '../context';
import * as styles from '../styles';
import type { NavigatorScreenProps } from '../types';

const animationEnterDelay = 0;
const animationEnterDuration = 0.14;
const animationExitDuration = 0.14;
const animationExitDelay = 0;

// Props specific to `framer-motion` can't be currently passed to `NavigatorScreen`,
// as some of them would overlap with HTML props (e.g. `onAnimationStart`, ...)
type Props = Omit<
WordPressComponentProps< NavigatorScreenProps, 'div', false >,
Exclude< keyof MotionProps, 'style' | 'children' >
>;

function UnconnectedNavigatorScreen(
props: Props,
props: WordPressComponentProps< NavigatorScreenProps, 'div', false >,
forwardedRef: ForwardedRef< any >
) {
const screenId = useId();
Expand All @@ -55,7 +39,6 @@ function UnconnectedNavigatorScreen(
'NavigatorScreen'
);

const prefersReducedMotion = useReducedMotion();
const { location, match, addScreen, removeScreen } =
useContext( NavigatorContext );
const isMatch = match === screenId;
Expand All @@ -70,19 +53,20 @@ function UnconnectedNavigatorScreen(
return () => removeScreen( screen );
}, [ screenId, path, addScreen, removeScreen ] );

const isRTL = isRTLFn();
const { isInitial, isBack } = location;
const cx = useCx();
const classes = useMemo(
() =>
cx(
css( {
// Ensures horizontal overflow is visually accessible.
overflowX: 'auto',
// In case the root has a height, it should not be exceeded.
maxHeight: '100%',
styles.navigatorScreen( {
isInitial,
isBack,
isRTL,
} ),
className
),
[ className, cx ]
[ className, cx, isInitial, isBack, isRTL ]
);

const locationRef = useRef( location );
Expand Down Expand Up @@ -149,73 +133,11 @@ function UnconnectedNavigatorScreen(

const mergedWrapperRef = useMergeRefs( [ forwardedRef, wrapperRef ] );

if ( ! isMatch ) {
return null;
}

if ( prefersReducedMotion ) {
return (
<View
ref={ mergedWrapperRef }
className={ classes }
{ ...otherProps }
>
{ children }
</View>
);
}

const animate = {
opacity: 1,
transition: {
delay: animationEnterDelay,
duration: animationEnterDuration,
ease: 'easeInOut',
},
x: 0,
};
// Disable the initial animation if the screen is the very first screen to be
// rendered within the current `NavigatorProvider`.
const initial =
location.isInitial && ! location.isBack
? false
: {
opacity: 0,
x:
( isRTL() && location.isBack ) ||
( ! isRTL() && ! location.isBack )
? 50
: -50,
};
const exit = {
delay: animationExitDelay,
opacity: 0,
x:
( ! isRTL() && location.isBack ) || ( isRTL() && ! location.isBack )
? 50
: -50,
transition: {
duration: animationExitDuration,
ease: 'easeInOut',
},
};

const animatedProps = {
animate,
exit,
initial,
};

return (
<motion.div
ref={ mergedWrapperRef }
className={ classes }
{ ...otherProps }
{ ...animatedProps }
>
return isMatch ? (
<View ref={ mergedWrapperRef } className={ classes } { ...otherProps }>
{ children }
</motion.div>
);
</View>
) : null;
}

/**
Expand Down
71 changes: 71 additions & 0 deletions packages/components/src/navigator/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* External dependencies
*/
import { css, keyframes } from '@emotion/react';

export const navigatorProviderWrapper = css`
/* Prevents horizontal overflow while animating screen transitions */
overflow-x: hidden;
/* Mark this subsection of the DOM as isolated, providing performance benefits
* by limiting calculations of layout, style, paint, size, or any combination
* to a DOM subtree rather than the entire page.
*/
contain: strict;
`;

const fadeInFromRight = keyframes( {
'0%': {
opacity: 0,
transform: `translateX( 50px )`,
},
'100%': { opacity: 1, transform: 'none' },
} );

const fadeInFromLeft = keyframes( {
'0%': {
opacity: 0,
transform: `translateX( -50px )`,
},
'100%': { opacity: 1, transform: 'none' },
} );

type NavigatorScreenAnimationProps = {
isInitial?: boolean;
isBack?: boolean;
isRTL: boolean;
};

const navigatorScreenAnimation = ( {
isInitial,
isBack,
isRTL,
}: NavigatorScreenAnimationProps ) => {
if ( isInitial && ! isBack ) {
return;
}

const animationName =
( isRTL && isBack ) || ( ! isRTL && ! isBack )
? fadeInFromRight
: fadeInFromLeft;

return css`
animation-duration: 0.14s;
animation-timing-function: ease-in-out;
will-change: transform, opacity;
animation-name: ${ animationName };
@media ( prefers-reduced-motion ) {
animation-duration: 0s;
}
`;
};

export const navigatorScreen = ( props: NavigatorScreenAnimationProps ) => css`
/* Ensures horizontal overflow is visually accessible */
overflow-x: auto;
/* In case the root has a height, it should not be exceeded */
max-height: 100%;
${ navigatorScreenAnimation( props ) }
`;
64 changes: 0 additions & 64 deletions packages/components/src/navigator/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -769,68 +769,4 @@ describe( 'Navigator', () => {
).toHaveFocus();
} );
} );

describe( 'animation', () => {
it( 'should not animate the initial screen', async () => {
const onHomeAnimationStartSpy = jest.fn();

render(
<NavigatorProvider initialPath="/">
<NavigatorScreen
path="/"
onAnimationStart={ onHomeAnimationStartSpy }
>
<CustomNavigatorButton path="/child">
To child
</CustomNavigatorButton>
</NavigatorScreen>
</NavigatorProvider>
);

expect( onHomeAnimationStartSpy ).not.toHaveBeenCalled();
} );

it( 'should animate all other screens (including the initial screen when navigating back)', async () => {
const user = userEvent.setup();

const onHomeAnimationStartSpy = jest.fn();
const onChildAnimationStartSpy = jest.fn();

render(
<NavigatorProvider initialPath="/">
<NavigatorScreen
path="/"
onAnimationStart={ onHomeAnimationStartSpy }
>
<CustomNavigatorButton path="/child">
To child
</CustomNavigatorButton>
</NavigatorScreen>
<NavigatorScreen
path="/child"
onAnimationStart={ onChildAnimationStartSpy }
>
<CustomNavigatorBackButton>
Back to home
</CustomNavigatorBackButton>
</NavigatorScreen>
</NavigatorProvider>
);

expect( onHomeAnimationStartSpy ).not.toHaveBeenCalled();
expect( onChildAnimationStartSpy ).not.toHaveBeenCalled();

await user.click(
screen.getByRole( 'button', { name: 'To child' } )
);
expect( onChildAnimationStartSpy ).toHaveBeenCalledTimes( 1 );
expect( onHomeAnimationStartSpy ).not.toHaveBeenCalled();

await user.click(
screen.getByRole( 'button', { name: 'Back to home' } )
);
expect( onChildAnimationStartSpy ).toHaveBeenCalledTimes( 1 );
expect( onHomeAnimationStartSpy ).toHaveBeenCalledTimes( 1 );
} );
} );
} );

0 comments on commit db81497

Please sign in to comment.