diff --git a/packages/mui-base/src/utils/useExecuteIfNotAnimated.ts b/packages/mui-base/src/utils/useExecuteIfNotAnimated.ts new file mode 100644 index 000000000..e07f926ec --- /dev/null +++ b/packages/mui-base/src/utils/useExecuteIfNotAnimated.ts @@ -0,0 +1,49 @@ +'use client'; +import * as React from 'react'; +import { useEventCallback } from './useEventCallback'; +import { ownerWindow } from './owner'; + +/** + * Executes a function only if the given element has no CSS animations or transitions. + * @ignore - internal hook. + */ +export function useExecuteIfNotAnimated(getElement: () => Element | null | undefined) { + const frame1Ref = React.useRef(-1); + const frame2Ref = React.useRef(-1); + + const cancelFrames = useEventCallback(() => { + cancelAnimationFrame(frame1Ref.current); + cancelAnimationFrame(frame2Ref.current); + }); + + React.useEffect(() => cancelFrames, [cancelFrames]); + + return useEventCallback((fnToExecute: () => void) => { + 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(() => { + 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) { + fnToExecute(); + } + }); + }); + }); +} diff --git a/packages/mui-base/src/utils/useTransitionStatus.ts b/packages/mui-base/src/utils/useTransitionStatus.ts new file mode 100644 index 000000000..0cef427b8 --- /dev/null +++ b/packages/mui-base/src/utils/useTransitionStatus.ts @@ -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(); + 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, + transitionStatus, + }), + [mounted, transitionStatus], + ); +}