mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
refactor(toast): animation & toast region (#5647)
* fix(toast): unexpected gap after closing a toast * refactor(toast): animation * fix(toast): close toast when disableAnimation is set to true * refactor: coderabbit comment
This commit is contained in:
parent
afe2f977fc
commit
e0dc33f3b6
5
.changeset/tasty-teachers-grin.md
Normal file
5
.changeset/tasty-teachers-grin.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@heroui/toast": patch
|
||||
---
|
||||
|
||||
refactor: toast
|
||||
@ -6,7 +6,7 @@ export type {ToastProps} from "./toast";
|
||||
|
||||
// export hooks
|
||||
export {useToast} from "./use-toast";
|
||||
export {addToast, closeAll, closeToast, getToastQueue} from "./toast-provider";
|
||||
export {addToast, closeAll, closeToast, getToastQueue, isToastClosing} from "./toast-provider";
|
||||
|
||||
// export component
|
||||
export {Toast};
|
||||
|
||||
@ -68,11 +68,23 @@ export const addToast = ({...props}: ToastProps & ToastOptions) => {
|
||||
return globalToastQueue.add(props);
|
||||
};
|
||||
|
||||
const closingToasts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
export const closeToast = (key: string) => {
|
||||
if (!globalToastQueue) {
|
||||
return;
|
||||
}
|
||||
globalToastQueue.close(key);
|
||||
|
||||
if (closingToasts.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
closingToasts.delete(key);
|
||||
globalToastQueue?.close(key);
|
||||
}, 300);
|
||||
|
||||
closingToasts.set(key, timeoutId);
|
||||
};
|
||||
|
||||
export const closeAll = () => {
|
||||
@ -80,11 +92,11 @@ export const closeAll = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = globalToastQueue.visibleToasts.map((toast) => toast.key);
|
||||
const toasts = [...globalToastQueue.visibleToasts];
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
setTimeout(() => {
|
||||
globalToastQueue?.close(key);
|
||||
}, index * 100);
|
||||
toasts.forEach((toast) => {
|
||||
closeToast(toast.key);
|
||||
});
|
||||
};
|
||||
|
||||
export const isToastClosing = (key: string) => closingToasts.has(key);
|
||||
|
||||
@ -11,6 +11,7 @@ import {clsx, mergeProps} from "@heroui/shared-utils";
|
||||
import {AnimatePresence} from "framer-motion";
|
||||
|
||||
import Toast from "./toast";
|
||||
import {isToastClosing} from "./toast-provider";
|
||||
|
||||
export interface RegionProps {
|
||||
className?: string;
|
||||
@ -93,12 +94,14 @@ export function ToastRegion<T extends ToastProps>({
|
||||
total - index <= 4 ||
|
||||
(isHovered && total - index <= maxVisibleToasts + 1)
|
||||
) {
|
||||
const isClosing = isToastClosing(toast.key);
|
||||
|
||||
return (
|
||||
<Toast
|
||||
key={toast.key}
|
||||
state={toastQueue}
|
||||
toast={toast}
|
||||
{...mergeProps(toastProps, toast.content)}
|
||||
{...mergeProps(toastProps, toast.content, {isClosing})}
|
||||
disableAnimation={disableAnimation}
|
||||
heights={heights}
|
||||
index={index}
|
||||
|
||||
@ -109,23 +109,7 @@ const Toast = forwardRef<"div", ToastProps>((props, ref) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{disableAnimation ? (
|
||||
toastContent
|
||||
) : (
|
||||
<m.div {...getMotionDivProps()}>
|
||||
<m.div
|
||||
key={"inner-div"}
|
||||
animate={{opacity: 1}}
|
||||
exit={{opacity: 0}}
|
||||
initial={{opacity: 0}}
|
||||
transition={{duration: 0.25, ease: "easeOut", delay: 0.1}}
|
||||
>
|
||||
{toastContent}
|
||||
</m.div>
|
||||
</m.div>
|
||||
)}
|
||||
</>
|
||||
<>{disableAnimation ? toastContent : <m.div {...getMotionDivProps()}>{toastContent}</m.div>}</>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -110,6 +110,10 @@ export interface ToastProps extends ToastVariantProps {
|
||||
* @default "default"
|
||||
*/
|
||||
severity?: "default" | "primary" | "secondary" | "success" | "warning" | "danger";
|
||||
/**
|
||||
* Whether the toast is being closed programmatically
|
||||
*/
|
||||
isClosing?: boolean;
|
||||
}
|
||||
|
||||
interface Props<T> extends Omit<HTMLHeroUIProps<"div">, "title">, ToastProps {
|
||||
@ -165,6 +169,7 @@ export function useToast<T extends ToastProps>(originalProps: UseToastProps<T>)
|
||||
severity,
|
||||
maxVisibleToasts,
|
||||
loadingComponent,
|
||||
isClosing = false,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@ -210,6 +215,18 @@ export function useToast<T extends ToastProps>(originalProps: UseToastProps<T>)
|
||||
});
|
||||
}, [promiseProp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isClosing && !isToastExiting) {
|
||||
setIsToastExiting(true);
|
||||
}
|
||||
}, [isClosing, isToastExiting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isToastExiting && disableAnimation) {
|
||||
state.close(toast.key);
|
||||
}
|
||||
}, [isToastExiting, disableAnimation, state, toast.key]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateProgress = (timestamp: number) => {
|
||||
if (!timeout || isLoading) {
|
||||
@ -290,6 +307,7 @@ export function useToast<T extends ToastProps>(originalProps: UseToastProps<T>)
|
||||
if (!domRef.current || !mounted || isToastExiting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toastNode = domRef.current;
|
||||
const originalHeight = toastNode.style.height;
|
||||
|
||||
@ -315,7 +333,7 @@ export function useToast<T extends ToastProps>(originalProps: UseToastProps<T>)
|
||||
let liftHeight = 4;
|
||||
|
||||
for (let idx = index + 1; idx < total; idx++) {
|
||||
liftHeight += heights[idx];
|
||||
liftHeight += heights[idx] || 0;
|
||||
}
|
||||
|
||||
const frontHeight = heights[heights.length - 1];
|
||||
@ -413,16 +431,12 @@ export function useToast<T extends ToastProps>(originalProps: UseToastProps<T>)
|
||||
"data-toast": true,
|
||||
"aria-label": "toast",
|
||||
"data-toast-exiting": dataAttr(isToastExiting),
|
||||
onTransitionEnd: () => {
|
||||
if (isToastExiting) {
|
||||
const updatedHeights = heights;
|
||||
|
||||
updatedHeights.splice(index, 1);
|
||||
setHeights([...updatedHeights]);
|
||||
|
||||
state.close(toast.key);
|
||||
}
|
||||
},
|
||||
onTransitionEnd: disableAnimation
|
||||
? undefined
|
||||
: () => {
|
||||
if (!isToastExiting) return;
|
||||
state.close(toast.key);
|
||||
},
|
||||
style: {
|
||||
opacity: opacityValue,
|
||||
...pseudoElementStyles,
|
||||
@ -441,6 +455,7 @@ export function useToast<T extends ToastProps>(originalProps: UseToastProps<T>)
|
||||
isToastExiting,
|
||||
state,
|
||||
toast.key,
|
||||
disableAnimation,
|
||||
],
|
||||
);
|
||||
|
||||
@ -580,7 +595,10 @@ export function useToast<T extends ToastProps>(originalProps: UseToastProps<T>)
|
||||
},
|
||||
drag: dragDirection,
|
||||
dragConstraints,
|
||||
exit: {opacity: 0},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
transition: {duration: 0.3},
|
||||
},
|
||||
initial: {opacity: 0, scale: 1, y: -40 * multiplier},
|
||||
transition: {duration: 0.3, ease: "easeOut"},
|
||||
variants: toastVariants,
|
||||
@ -591,12 +609,7 @@ export function useToast<T extends ToastProps>(originalProps: UseToastProps<T>)
|
||||
setDrag(false);
|
||||
|
||||
if (shouldCloseToast(offsetX, offsetY)) {
|
||||
const updatedHeights = heights;
|
||||
|
||||
updatedHeights.splice(index, 1);
|
||||
setHeights([...updatedHeights]);
|
||||
|
||||
state.close(toast.key);
|
||||
setIsToastExiting(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user