UNPKG

@base-ui/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

97 lines (94 loc) 4.16 kB
"use strict"; 'use client'; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.useAnimationsFinished = useAnimationsFinished; var ReactDOM = _interopRequireWildcard(require("react-dom")); var _useAnimationFrame = require("@base-ui/utils/useAnimationFrame"); var _useStableCallback = require("@base-ui/utils/useStableCallback"); var _resolveRef = require("./resolveRef"); var _stateAttributesMapping = require("./stateAttributesMapping"); /** * Executes a function once all animations have finished on the provided element. * @param elementOrRef - The element to watch for animations. * @param waitForStartingStyleRemoved - Whether to wait for [data-starting-style] to be removed before checking for animations. * @param treatAbortedAsFinished - Whether to treat aborted animations as finished. If `false`, and there are aborted animations, * the function will check again if any new animations have started and wait for them to finish. * @returns A function that takes a callback to execute once all animations have finished, and an optional AbortSignal to abort the callback */ function useAnimationsFinished(elementOrRef, waitForStartingStyleRemoved = false, treatAbortedAsFinished = true) { const frame = (0, _useAnimationFrame.useAnimationFrame)(); return (0, _useStableCallback.useStableCallback)((fnToExecute, /** * An optional [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that * can be used to abort `fnToExecute` before all the animations have finished. * @default null */ signal = null) => { frame.cancel(); function done() { // Synchronously flush the unmounting of the component so that the browser doesn't // paint: https://github.com/mui/base-ui/issues/979 ReactDOM.flushSync(fnToExecute); } const element = (0, _resolveRef.resolveRef)(elementOrRef); if (element == null) { return; } const resolvedElement = element; if (typeof resolvedElement.getAnimations !== 'function' || globalThis.BASE_UI_ANIMATIONS_DISABLED) { fnToExecute(); } else { function execWaitForStartingStyleRemoved() { const startingStyleAttribute = _stateAttributesMapping.TransitionStatusDataAttributes.startingStyle; // If `[data-starting-style]` isn't present, fall back to waiting one more frame // to give "open" animations a chance to be registered. if (!resolvedElement.hasAttribute(startingStyleAttribute)) { frame.request(exec); return; } // Wait for `[data-starting-style]` to have been removed. const attributeObserver = new MutationObserver(() => { if (!resolvedElement.hasAttribute(startingStyleAttribute)) { attributeObserver.disconnect(); exec(); } }); attributeObserver.observe(resolvedElement, { attributes: true, attributeFilter: [startingStyleAttribute] }); signal?.addEventListener('abort', () => attributeObserver.disconnect(), { once: true }); } function exec() { Promise.all(resolvedElement.getAnimations().map(anim => anim.finished)).then(() => { if (signal?.aborted) { return; } done(); }).catch(() => { const currentAnimations = resolvedElement.getAnimations(); if (treatAbortedAsFinished) { if (signal?.aborted) { return; } done(); } else if (currentAnimations.length > 0 && currentAnimations.some(anim => anim.pending || anim.playState !== 'finished')) { // Sometimes animations can be aborted because a property they depend on changes while the animation plays. // In such cases, we need to re-check if any new animations have started. exec(); } }); } if (waitForStartingStyleRemoved) { execWaitForStartingStyleRemoved(); return; } frame.request(exec); } }); }