-
-
Notifications
You must be signed in to change notification settings - Fork 44
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
[core] Add useTransitionStatus
and useExecuteIfNotAnimated
Hooks
#396
Changes from 8 commits
f08177a
aab360c
3a0c40f
317e15c
1bd9c96
de67aaa
41ca5d4
572b39e
0aee256
b5c071c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
'use client'; | ||
import * as React from 'react'; | ||
import { useEventCallback } from './useEventCallback'; | ||
import { ownerWindow } from './owner'; | ||
|
||
/** | ||
* Unmounts the supplied element only if it has no animations or transitions. | ||
* @ignore - internal hook. | ||
*/ | ||
export function useAnimationUnmount( | ||
getElement: () => Element | null | undefined, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this a function and not a ref object? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because of the wrapper in anchored popups, it's the The usage is: const unmount = useAnimationUnmount(
() => popupEl?.firstElementChild,
() => setMounted(false)
); |
||
onUnmount: () => void, | ||
) { | ||
const frame1Ref = React.useRef(-1); | ||
const frame2Ref = React.useRef(-1); | ||
|
||
const cancelFrames = useEventCallback(() => { | ||
cancelAnimationFrame(frame1Ref.current); | ||
cancelAnimationFrame(frame2Ref.current); | ||
}); | ||
|
||
React.useEffect(() => cancelFrames, [cancelFrames]); | ||
|
||
const unmount = useEventCallback(() => { | ||
cancelFrames(); | ||
|
||
const element = getElement(); | ||
|
||
if (!element) { | ||
return; | ||
} | ||
|
||
// Wait for the CSS styles to be applied to determine if the animation has been removed in the | ||
// [data-instant] state. This allows the close animation to play if the `delay` instantType is | ||
// applying to the same element. | ||
// Notes: | ||
// - A single `requestAnimationFrame` is sometimes unreliable. | ||
// - `queueMicrotask` does not work. | ||
frame1Ref.current = requestAnimationFrame(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if two animation frames are guaranteed to be reliable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Playing around with the transitions experiments demo with |
||
frame2Ref.current = requestAnimationFrame(() => { | ||
const computedStyles = ownerWindow(element).getComputedStyle(element); | ||
const hasNoAnimation = | ||
['', 'none'].includes(computedStyles.animationName) || | ||
['', '0s'].includes(computedStyles.animationDuration); | ||
const hasNoTransition = ['', '0s'].includes(computedStyles.transitionDuration); | ||
|
||
if (hasNoAnimation && hasNoTransition) { | ||
onUnmount(); | ||
} | ||
}); | ||
}); | ||
}); | ||
|
||
return unmount; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
'use client'; | ||
import * as React from 'react'; | ||
import { useEnhancedEffect } from './useEnhancedEffect'; | ||
|
||
export type TransitionStatus = 'entering' | 'exiting' | undefined; | ||
|
||
/** | ||
* Provides a status string for CSS animations. | ||
* @param open - a boolean that determines if the element is open. | ||
* @ignore - internal hook. | ||
*/ | ||
export function useTransitionStatus(open: boolean) { | ||
const [transitionStatus, setTransitionStatus] = React.useState<TransitionStatus>(); | ||
const [mounted, setMounted] = React.useState(open); | ||
|
||
if (open && !mounted) { | ||
setMounted(true); | ||
} | ||
|
||
if (!open && mounted && transitionStatus !== 'exiting') { | ||
setTransitionStatus('exiting'); | ||
} | ||
|
||
if (!open && !mounted && transitionStatus === 'exiting') { | ||
setTransitionStatus(undefined); | ||
} | ||
|
||
useEnhancedEffect(() => { | ||
if (!open) { | ||
return undefined; | ||
} | ||
|
||
setTransitionStatus('entering'); | ||
|
||
const frame = requestAnimationFrame(() => { | ||
setTransitionStatus(undefined); | ||
}); | ||
|
||
return () => { | ||
cancelAnimationFrame(frame); | ||
}; | ||
}, [open]); | ||
|
||
return React.useMemo( | ||
() => ({ | ||
mounted, | ||
setMounted, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make sense to call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Example: function handleEnd({ target }: React.SyntheticEvent) {
const popupElement = refs.floating.current?.firstElementChild;
if (target === popupElement && setMounted) {
setMounted((prevMounted) => (prevMounted ? false : prevMounted));
}
} |
||
transitionStatus, | ||
}), | ||
[mounted, transitionStatus], | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find it quite hard to understand the purpose of this function when reading the code. It doesn't really unmount anything (as the name would suggest), but it's a means of detecting if the element has any animation defined in its styles. Renaming it could make it easier to grasp.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I didn't really know what to call it without it being really long. It returns an
unmount
function, but it's simply guarded to only run if the element has no animations/transitions.useUnmountIfNoAnimations
useCallIfNoAnimations
(well, since the supplied function can be anything)