Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions packages/bui-core/src/Fade/Fade.miniapp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* Fade Animation Component
* @description A component that implements fade in/out animation effects for elements
* @component Fade
*/
import React, { useEffect, useState, useRef } from 'react';
import {
createTransitions,
duration,
easing,
getTransitionProps,
useForkRef,
} from '@bifrostui/utils';
import type { FadeProps } from './Fade.types';
import './index.less';

const defaultEasing = {
enter: easing.easeOut,
exit: easing.sharp,
};

const defaultTimeout = {
enter: duration.enteringScreen,
exit: duration.leavingScreen,
};

const Fade = React.forwardRef<HTMLElement, FadeProps>((props, ref) => {
const {
// Base props
children,
in: inProp,
style,
// Animation controls
appear = true,
enter = true,
exit = true,
delay = 0,
easing: easingProp = defaultEasing,
timeout = defaultTimeout,
// Lifecycle hooks
mountOnEnter,
unmountOnExit,
// Animation callbacks
onEnter,
onEntering,
onEntered,
onExit,
onExiting,
onExited,
// Other props
...others
} = props;

const isFirstMount = useRef(true);
const [isMounted, setIsMounted] = useState(
() => !mountOnEnter || !unmountOnExit,
);
const elementRef = useRef(null);
// @ts-expect-error will upstream fix
const handleRef = useForkRef(ref, children?.ref, elementRef);
// Whether to animate on first mount
const shouldAnimateOnFirstMount = inProp && appear;
// Whether to animate on subsequent updates
const shouldAnimate = (inProp && enter) || (!inProp && exit);
/**
* Animation configuration
*/
const getAnimationDuration = () => {
if (isFirstMount.current) {
return shouldAnimateOnFirstMount ? timeout : 0;
}
return shouldAnimate ? timeout : 0;
};

// Whether to skip the initial animation
const shouldSkipFirstAnimation =
isFirstMount.current && !shouldAnimateOnFirstMount;

/**
* Generate animation configuration
*/
const transitions = createTransitions();
const animationName = inProp ? 'bui-fade-in' : 'bui-fade-out';
const animationDuration = getAnimationDuration();
const animation = transitions.create(
animationName,
getTransitionProps(
{
timeout: animationDuration,
style,
easing: easingProp,
delay,
},
{ mode: inProp ? 'enter' : 'exit' },
),
);

/**
* Lifecycle management
*/
useEffect(() => {
// Control component mount state
if (inProp && !isMounted) {
setIsMounted(true);
}
}, [inProp, isMounted]);

useEffect(() => {
// Update first render state
if (isMounted && isFirstMount.current) {
isFirstMount.current = false;
}
}, [isMounted]);

/**
* Animation event handlers
*/
useEffect(() => {
// Trigger animation start callback
const shouldTriggerCallback = isMounted && animationDuration !== 0;
if (!shouldTriggerCallback) return;

if (inProp) {
onEnter?.(elementRef.current);
} else {
onExit?.(elementRef.current);
}
}, [inProp, isMounted, animationDuration, onEnter, onExit]);

const handleAnimationStart = () => {
if (animationDuration === 0) return;

if (inProp) {
onEntering?.(elementRef.current);
} else {
onExiting?.(elementRef.current);
}
};

const handleAnimationEnd = () => {
if (shouldSkipFirstAnimation) return;

if (inProp) {
onEntered?.(elementRef.current);
} else {
onExited?.(elementRef.current);
if (unmountOnExit) {
setIsMounted(false);
}
}
};
/**
* Render
*/
if (!children || !isMounted) return null;

return React.cloneElement(children, {
...others,
ref: handleRef,
onAnimationEnd: handleAnimationEnd,
onAnimationStart: handleAnimationStart,
style: {
animation,
animationFillMode: 'forwards',
...style,
...children.props?.style,
},
});
});

Fade.displayName = 'BuiFade';

export default Fade;
22 changes: 13 additions & 9 deletions packages/bui-core/src/Fade/Fade.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,19 @@ const Fade = React.forwardRef<HTMLElement, FadeProps>(
ref={nodeRef}
>
{(state, childProps) => {
const transition = transitions.create(
'opacity',
getTransitionProps(
{ timeout, style, easing: easingProp, delay },
{
mode: state,
},
),
);
const transition =
state === 'entering' || state === 'exiting'
? transitions.create(
'opacity',
getTransitionProps(
{ timeout, style, easing: easingProp, delay },
{
mode: state,
},
),
)
: 'none';

return React.cloneElement(children, {
style: {
visibility: state === 'exited' ? 'hidden' : 'visible',
Expand Down
20 changes: 20 additions & 0 deletions packages/bui-core/src/Fade/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* 简单的进入动画 */
@keyframes bui-fade-in {
from {
opacity: 0;
}

to {
opacity: 1;
}
}

@keyframes bui-fade-out {
from {
opacity: 1;
}

to {
opacity: 0;
}
}
4 changes: 4 additions & 0 deletions packages/bui-core/src/Modal/Modal.miniapp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const Modal = React.forwardRef<HTMLDivElement, ViewProps & ModalProps>(
disableScrollLock = false,
hideBackdrop = false,
onClose,
onClick,
keepMounted,
...others
} = props;
Expand Down Expand Up @@ -49,6 +50,9 @@ const Modal = React.forwardRef<HTMLDivElement, ViewProps & ModalProps>(
className={clsx(prefixCls, className)}
ref={handleRef}
catchMove={!disableScrollLock}
onClick={(event) => {
onClick?.(event);
}}
{...others}
>
{!hideBackdrop ? (
Expand Down
Loading
Loading