Skip to content

Commit

Permalink
presence: clear timeout if node changes
Browse files Browse the repository at this point in the history
  • Loading branch information
chaance committed Sep 26, 2024
1 parent 5a03e97 commit 5510111
Show file tree
Hide file tree
Showing 2 changed files with 23 additions and 46 deletions.
43 changes: 5 additions & 38 deletions packages/react/presence/src/Presence.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export const WithDeferredMountAnimation = () => {
const timerRef = React.useRef(0);
const [open, setOpen] = React.useState(false);
const [animate, setAnimate] = React.useState(false);
const [animationEndCount, setAnimationEndCount] = React.useState(0);

React.useEffect(() => {
if (open) {
Expand All @@ -46,28 +45,15 @@ export const WithDeferredMountAnimation = () => {
}
}, [open]);

const handleAnimationEnd = React.useCallback(() => {
setAnimationEndCount((count) => count + 1);
}, []);

return (
<>
<p>
Deferred animation should unmount correctly when toggled. Content will flash briefly while
we wait for animation to be applied.
</p>
<Toggles
nodeRef={ref}
open={open}
onOpenChange={setOpen}
animationEndCount={animationEndCount}
/>
<Toggles nodeRef={ref} open={open} onOpenChange={setOpen} />
<Presence present={open}>
<div
className={animate ? mountAnimationClass() : undefined}
onAnimationEnd={handleAnimationEnd}
ref={ref}
>
<div className={animate ? mountAnimationClass() : undefined} ref={ref}>
Content
</div>
</Presence>
Expand All @@ -78,35 +64,20 @@ export const WithDeferredMountAnimation = () => {
function Animation(props: React.ComponentProps<'div'>) {
const ref = React.useRef<HTMLDivElement>(null);
const [open, setOpen] = React.useState(false);
const [animationEndCount, setAnimationEndCount] = React.useState(0);

const handleAnimationEnd = React.useCallback(() => {
setAnimationEndCount((count) => count + 1);
}, []);

return (
<>
<Toggles
nodeRef={ref}
open={open}
onOpenChange={setOpen}
animationEndCount={animationEndCount}
/>
<Toggles nodeRef={ref} open={open} onOpenChange={setOpen} />
<Presence present={open}>
<div
{...props}
data-state={open ? 'open' : 'closed'}
onAnimationEnd={handleAnimationEnd}
ref={ref}
>
<div {...props} data-state={open ? 'open' : 'closed'} ref={ref}>
Content
</div>
</Presence>
</>
);
}

function Toggles({ open, onOpenChange, animationEndCount, nodeRef }: any) {
function Toggles({ open, onOpenChange, nodeRef }: any) {
function handleToggleVisibility() {
const node = nodeRef.current;
if (node) {
Expand All @@ -132,10 +103,6 @@ function Toggles({ open, onOpenChange, animationEndCount, nodeRef }: any) {
toggle
</button>
</fieldset>
<fieldset>
<legend>Animation end counter</legend>
<output>{animationEndCount}</output>
</fieldset>
</form>
);
}
Expand Down
26 changes: 18 additions & 8 deletions packages/react/presence/src/Presence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ function usePresence(present: boolean) {

useLayoutEffect(() => {
if (node) {
let timeoutId: number;
const ownerWindow = node.ownerDocument.defaultView ?? window;
/**
* Triggering an ANIMATION_OUT during an ANIMATION_IN will fire an `animationcancel`
* event for ANIMATION_IN after we have entered `unmountSuspended` state. So, we
Expand All @@ -100,18 +102,25 @@ function usePresence(present: boolean) {
const currentAnimationName = getAnimationName(stylesRef.current);
const isCurrentAnimation = currentAnimationName.includes(event.animationName);
if (event.target === node && isCurrentAnimation) {
// With React 18 concurrency this update is applied a frame after the animation
// ends, creating a flash of visible content. By setting the animation fill mode
// to "forwards", we force the node to keep the styles of the last keyframe,
// removing the flash.
// With React 18 concurrency this update is applied a frame after the
// animation ends, creating a flash of visible content. By setting the
// animation fill mode to "forwards", we force the node to keep the
// styles of the last keyframe, removing the flash.
//
// Previously we flushed the update via ReactDom.flushSync, but with
// exit animations this resulted in the node being removed from the
// DOM before the synthetic animationEnd event was dispatched, meaning
// user-provided event handlers would not be called.
// https://github.com/radix-ui/primitives/pull/1849
send('ANIMATION_END');
if (!prevPresentRef.current) {
const currentFillMode = node.style.animationFillMode;
node.style.animationFillMode = 'forwards';
// Reset the style after the node had time to unmount (for cases where the
// component chooses not to unmount). Doing this any sooner than `setTimeout`
// (e.g. with `requestAnimationFrame`) still causes a flash.
setTimeout(() => {
// Reset the style after the node had time to unmount (for cases
// where the component chooses not to unmount). Doing this any
// sooner than `setTimeout` (e.g. with `requestAnimationFrame`)
// still causes a flash.
timeoutId = ownerWindow.setTimeout(() => {
if (node.style.animationFillMode === 'forwards') {
node.style.animationFillMode = currentFillMode;
}
Expand All @@ -129,6 +138,7 @@ function usePresence(present: boolean) {
node.addEventListener('animationcancel', handleAnimationEnd);
node.addEventListener('animationend', handleAnimationEnd);
return () => {
ownerWindow.clearTimeout(timeoutId);
node.removeEventListener('animationstart', handleAnimationStart);
node.removeEventListener('animationcancel', handleAnimationEnd);
node.removeEventListener('animationend', handleAnimationEnd);
Expand Down

0 comments on commit 5510111

Please sign in to comment.