forked from WordPress/gutenberg
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Navigator: add support for exit animation (WordPress#64777)
* Refactor screen logic for better clarity and hook deps * Add exit animation, rewrite screen animations / DOM rendering logic * Only add inset CSS rule when animating out * Parametrise animation * CHANGELOG * Add fallback timeout * Mention wrapper height in README * Use `useReducedMotion()` hook instead of custom logic * Extract useScreenAnimatePresence hook, tidy up * Forward animationEnd * Add `setWrapperHeight` functionality via context * Use `clip` instead of `hidden` for overflow-x * Less aggressive clipping for screens that are animating out * Better sizing styles for screen, to keep it more stable while transitioning out * Refine internal animation logic for less jumpy animations * Remove unnecessary Storybook styles * Change wording * Improve logic: - more clear animation status names - apply CSS animation only while effectively animating - fix bug in the animationEnd callback which was matching animation end events too loosely, thus causing glitches * Add timeout fallback for in animations too * Fix animation delay for forwards.out animation * Use display: grid instead of absolute positioning to set provider min height Remove unnecessary import * Simplify navigatorScreenAnimation * Remove unnecessary state element ref * Use "start" and "end" instead of "backwards" and "forwards" * Do not rely on `usePrevious` * Use CSS transitions * Fix Storybook example * Revert "Use CSS transitions" This reverts commit 946f9c953232b788f58050d2a94d9d131527a180. * Switch to data-attributes for less runtime emotion calculations * Add back fallback animation timeout * Move CHANGELOG entry to unreleased section * Clean up code, reduce diff for easier reviewing --- Co-authored-by: ciampo <[email protected]> Co-authored-by: mirka <[email protected]> Co-authored-by: tyxla <[email protected]> Co-authored-by: jsnajdr <[email protected]> Co-authored-by: jasmussen <[email protected]>
- Loading branch information
1 parent
3679084
commit 69efbca
Showing
6 changed files
with
346 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
177 changes: 177 additions & 0 deletions
177
packages/components/src/navigator/navigator-screen/use-screen-animate-presence.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { | ||
useState, | ||
useEffect, | ||
useLayoutEffect, | ||
useCallback, | ||
} from '@wordpress/element'; | ||
import { useReducedMotion } from '@wordpress/compose'; | ||
import { isRTL as isRTLFn } from '@wordpress/i18n'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import * as styles from '../styles'; | ||
|
||
// Possible values: | ||
// - 'INITIAL': the initial state | ||
// - 'ANIMATING_IN': start enter animation | ||
// - 'IN': enter animation has ended | ||
// - 'ANIMATING_OUT': start exit animation | ||
// - 'OUT': the exit animation has ended | ||
type AnimationStatus = | ||
| 'INITIAL' | ||
| 'ANIMATING_IN' | ||
| 'IN' | ||
| 'ANIMATING_OUT' | ||
| 'OUT'; | ||
|
||
// Allow an extra 20% of the total animation duration to account for potential | ||
// event loop delays. | ||
const ANIMATION_TIMEOUT_MARGIN = 1.2; | ||
|
||
const isEnterAnimation = ( | ||
animationDirection: 'end' | 'start', | ||
animationStatus: AnimationStatus, | ||
animationName: string | ||
) => | ||
animationStatus === 'ANIMATING_IN' && | ||
animationName === styles.ANIMATION_END_NAMES[ animationDirection ].in; | ||
|
||
const isExitAnimation = ( | ||
animationDirection: 'end' | 'start', | ||
animationStatus: AnimationStatus, | ||
animationName: string | ||
) => | ||
animationStatus === 'ANIMATING_OUT' && | ||
animationName === styles.ANIMATION_END_NAMES[ animationDirection ].out; | ||
|
||
export function useScreenAnimatePresence( { | ||
isMatch, | ||
skipAnimation, | ||
isBack, | ||
onAnimationEnd, | ||
}: { | ||
isMatch: boolean; | ||
skipAnimation: boolean; | ||
isBack?: boolean; | ||
onAnimationEnd?: React.AnimationEventHandler< Element >; | ||
} ) { | ||
const isRTL = isRTLFn(); | ||
const prefersReducedMotion = useReducedMotion(); | ||
|
||
const [ animationStatus, setAnimationStatus ] = | ||
useState< AnimationStatus >( 'INITIAL' ); | ||
|
||
// Start enter and exit animations when the screen is selected or deselected. | ||
// The animation status is set to `IN` or `OUT` immediately if the animation | ||
// should be skipped. | ||
const becameSelected = | ||
animationStatus !== 'ANIMATING_IN' && | ||
animationStatus !== 'IN' && | ||
isMatch; | ||
const becameUnselected = | ||
animationStatus !== 'ANIMATING_OUT' && | ||
animationStatus !== 'OUT' && | ||
! isMatch; | ||
useLayoutEffect( () => { | ||
if ( becameSelected ) { | ||
setAnimationStatus( | ||
skipAnimation || prefersReducedMotion ? 'IN' : 'ANIMATING_IN' | ||
); | ||
} else if ( becameUnselected ) { | ||
setAnimationStatus( | ||
skipAnimation || prefersReducedMotion ? 'OUT' : 'ANIMATING_OUT' | ||
); | ||
} | ||
}, [ | ||
becameSelected, | ||
becameUnselected, | ||
skipAnimation, | ||
prefersReducedMotion, | ||
] ); | ||
|
||
// Animation attributes (derived state). | ||
const animationDirection = | ||
( isRTL && isBack ) || ( ! isRTL && ! isBack ) ? 'end' : 'start'; | ||
const isAnimatingIn = animationStatus === 'ANIMATING_IN'; | ||
const isAnimatingOut = animationStatus === 'ANIMATING_OUT'; | ||
let animationType: 'in' | 'out' | undefined; | ||
if ( isAnimatingIn ) { | ||
animationType = 'in'; | ||
} else if ( isAnimatingOut ) { | ||
animationType = 'out'; | ||
} | ||
|
||
const onScreenAnimationEnd = useCallback( | ||
( e: React.AnimationEvent< HTMLElement > ) => { | ||
onAnimationEnd?.( e ); | ||
|
||
if ( | ||
isExitAnimation( | ||
animationDirection, | ||
animationStatus, | ||
e.animationName | ||
) | ||
) { | ||
// When the exit animation ends on an unselected screen, set the | ||
// status to 'OUT' to remove the screen contents from the DOM. | ||
setAnimationStatus( 'OUT' ); | ||
} else if ( | ||
isEnterAnimation( | ||
animationDirection, | ||
animationStatus, | ||
e.animationName | ||
) | ||
) { | ||
// When the enter animation ends on a selected screen, set the | ||
// status to 'IN' to ensure the screen is rendered in the DOM. | ||
setAnimationStatus( 'IN' ); | ||
} | ||
}, | ||
[ onAnimationEnd, animationStatus, animationDirection ] | ||
); | ||
|
||
// Fallback timeout to ensure that the logic is applied even if the | ||
// `animationend` event is not triggered. | ||
useEffect( () => { | ||
let animationTimeout: number | undefined; | ||
|
||
if ( isAnimatingOut ) { | ||
animationTimeout = window.setTimeout( () => { | ||
setAnimationStatus( 'OUT' ); | ||
animationTimeout = undefined; | ||
}, styles.TOTAL_ANIMATION_DURATION.OUT * ANIMATION_TIMEOUT_MARGIN ); | ||
} else if ( isAnimatingIn ) { | ||
animationTimeout = window.setTimeout( () => { | ||
setAnimationStatus( 'IN' ); | ||
animationTimeout = undefined; | ||
}, styles.TOTAL_ANIMATION_DURATION.IN * ANIMATION_TIMEOUT_MARGIN ); | ||
} | ||
|
||
return () => { | ||
if ( animationTimeout ) { | ||
window.clearTimeout( animationTimeout ); | ||
animationTimeout = undefined; | ||
} | ||
}; | ||
}, [ isAnimatingOut, isAnimatingIn ] ); | ||
|
||
return { | ||
animationStyles: styles.navigatorScreenAnimation, | ||
// Render the screen's contents in the DOM not only when the screen is | ||
// selected, but also while it is animating out. | ||
shouldRenderScreen: | ||
isMatch || | ||
animationStatus === 'IN' || | ||
animationStatus === 'ANIMATING_OUT', | ||
screenProps: { | ||
onAnimationEnd: onScreenAnimationEnd, | ||
'data-animation-direction': animationDirection, | ||
'data-animation-type': animationType, | ||
'data-skip-animation': skipAnimation || undefined, | ||
}, | ||
} as const; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.