Skip to content

Commit

Permalink
[core] Add useTransitionStatus and useExecuteIfNotAnimated Hooks (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
atomiks authored May 23, 2024
1 parent 20a55af commit bf8fca5
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 0 deletions.
49 changes: 49 additions & 0 deletions packages/mui-base/src/utils/useExecuteIfNotAnimated.ts
Original file line number Diff line number Diff line change
@@ -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();
}
});
});
});
}
52 changes: 52 additions & 0 deletions packages/mui-base/src/utils/useTransitionStatus.ts
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,
transitionStatus,
}),
[mounted, transitionStatus],
);
}

0 comments on commit bf8fca5

Please sign in to comment.