124 lines
5.1 KiB
JavaScript
124 lines
5.1 KiB
JavaScript
import { useEventCallback, useFirstMount, useIsomorphicLayoutEffect, useMergedRefs } from '@fluentui/react-utilities';
|
|
import * as React from 'react';
|
|
import { PresenceGroupChildContext } from '../contexts/PresenceGroupChildContext';
|
|
import { useAnimateAtoms } from '../hooks/useAnimateAtoms';
|
|
import { useMotionImperativeRef } from '../hooks/useMotionImperativeRef';
|
|
import { useMountedState } from '../hooks/useMountedState';
|
|
import { useIsReducedMotion } from '../hooks/useIsReducedMotion';
|
|
import { getChildElement } from '../utils/getChildElement';
|
|
import { useMotionBehaviourContext } from '../contexts/MotionBehaviourContext';
|
|
/**
|
|
* @internal A private symbol to store the motion definition on the component for variants.
|
|
*/ export const MOTION_DEFINITION = Symbol('MOTION_DEFINITION');
|
|
function shouldSkipAnimation(appear, isFirstMount, visible) {
|
|
return !appear && isFirstMount && !!visible;
|
|
}
|
|
export function createPresenceComponent(value) {
|
|
return Object.assign((props)=>{
|
|
'use no memo';
|
|
const itemContext = React.useContext(PresenceGroupChildContext);
|
|
const merged = {
|
|
...itemContext,
|
|
...props
|
|
};
|
|
const skipMotions = useMotionBehaviourContext() === 'skip';
|
|
const { appear, children, imperativeRef, onExit, onMotionFinish, onMotionStart, onMotionCancel, visible, unmountOnExit, ..._rest } = merged;
|
|
const params = _rest;
|
|
const [mounted, setMounted] = useMountedState(visible, unmountOnExit);
|
|
const child = getChildElement(children);
|
|
const handleRef = useMotionImperativeRef(imperativeRef);
|
|
const elementRef = React.useRef();
|
|
const ref = useMergedRefs(elementRef, child.ref);
|
|
const optionsRef = React.useRef({
|
|
appear,
|
|
params,
|
|
skipMotions
|
|
});
|
|
const animateAtoms = useAnimateAtoms();
|
|
const isFirstMount = useFirstMount();
|
|
const isReducedMotion = useIsReducedMotion();
|
|
const handleMotionStart = useEventCallback((direction)=>{
|
|
onMotionStart === null || onMotionStart === void 0 ? void 0 : onMotionStart(null, {
|
|
direction
|
|
});
|
|
});
|
|
const handleMotionFinish = useEventCallback((direction)=>{
|
|
onMotionFinish === null || onMotionFinish === void 0 ? void 0 : onMotionFinish(null, {
|
|
direction
|
|
});
|
|
if (direction === 'exit' && unmountOnExit) {
|
|
setMounted(false);
|
|
onExit === null || onExit === void 0 ? void 0 : onExit();
|
|
}
|
|
});
|
|
const handleMotionCancel = useEventCallback((direction)=>{
|
|
onMotionCancel === null || onMotionCancel === void 0 ? void 0 : onMotionCancel(null, {
|
|
direction
|
|
});
|
|
});
|
|
useIsomorphicLayoutEffect(()=>{
|
|
// Heads up!
|
|
// We store the params in a ref to avoid re-rendering the component when the params change.
|
|
optionsRef.current = {
|
|
appear,
|
|
params,
|
|
skipMotions
|
|
};
|
|
});
|
|
useIsomorphicLayoutEffect(()=>{
|
|
const element = elementRef.current;
|
|
if (!element || shouldSkipAnimation(optionsRef.current.appear, isFirstMount, visible)) {
|
|
return;
|
|
}
|
|
const presenceMotion = typeof value === 'function' ? value({
|
|
element,
|
|
...optionsRef.current.params
|
|
}) : value;
|
|
const atoms = visible ? presenceMotion.enter : presenceMotion.exit;
|
|
const direction = visible ? 'enter' : 'exit';
|
|
const applyInitialStyles = !visible && isFirstMount;
|
|
const skipAnimation = optionsRef.current.skipMotions;
|
|
if (!applyInitialStyles) {
|
|
handleMotionStart(direction);
|
|
}
|
|
const handle = animateAtoms(element, atoms, {
|
|
isReducedMotion: isReducedMotion()
|
|
});
|
|
if (applyInitialStyles) {
|
|
// Heads up!
|
|
// .finish() is used in this case to skip animation and apply animation styles immediately
|
|
handle.finish();
|
|
return;
|
|
}
|
|
handleRef.current = handle;
|
|
handle.setMotionEndCallbacks(()=>handleMotionFinish(direction), ()=>handleMotionCancel(direction));
|
|
if (skipAnimation) {
|
|
handle.finish();
|
|
}
|
|
return ()=>{
|
|
handle.cancel();
|
|
};
|
|
}, // Excluding `isFirstMount` from deps to prevent re-triggering the animation on subsequent renders
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[
|
|
animateAtoms,
|
|
handleRef,
|
|
isReducedMotion,
|
|
handleMotionFinish,
|
|
handleMotionStart,
|
|
handleMotionCancel,
|
|
visible
|
|
]);
|
|
if (mounted) {
|
|
return React.cloneElement(child, {
|
|
ref
|
|
});
|
|
}
|
|
return null;
|
|
}, {
|
|
// Heads up!
|
|
// Always normalize it to a function to simplify types
|
|
[MOTION_DEFINITION]: typeof value === 'function' ? value : ()=>value
|
|
});
|
|
}
|