fix(toast): trigger onClose after timeout (#5771)

This commit is contained in:
WK 2025-10-04 21:25:04 +08:00 committed by GitHub
parent 812f7939d9
commit ed76faacd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 88 additions and 6 deletions

View File

@ -0,0 +1,5 @@
---
"@heroui/toast": patch
---
trigger onClose after toast timeout (#5609)

View File

@ -174,4 +174,69 @@ describe("Toast", () => {
expect(loadingIcon).toBeTruthy();
});
it("should call onClose when toast times out", async () => {
const onCloseMock = jest.fn();
const wrapper = render(
<>
<ToastProvider disableAnimation />
<button
data-testid="button"
onClick={() => {
addToast({
title: title,
description: description,
timeout: 500,
onClose: onCloseMock,
});
}}
>
Show Toast
</button>
</>,
);
const button = wrapper.getByTestId("button");
await user.click(button);
await new Promise((resolve) => setTimeout(resolve, 1000));
expect(onCloseMock).toHaveBeenCalledTimes(1);
});
it("should call onClose when close button is clicked", async () => {
const onCloseMock = jest.fn();
const wrapper = render(
<>
<ToastProvider disableAnimation maxVisibleToasts={1} />
<button
data-testid="button"
onClick={() => {
addToast({
title: title,
description: description,
onClose: onCloseMock,
});
}}
>
Show Toast
</button>
</>,
);
const button = wrapper.getByTestId("button");
await user.click(button);
await new Promise((resolve) => setTimeout(resolve, 500));
const closeButtons = wrapper.getAllByRole("button");
await user.click(closeButtons[0]);
expect(onCloseMock).toHaveBeenCalledTimes(1);
});
});

View File

@ -10,7 +10,7 @@ import type {HTMLHeroUIProps, PropGetter} from "@heroui/system";
import {mapPropsVariants, useProviderContext} from "@heroui/system";
import {toast as toastTheme} from "@heroui/theme";
import {useDOMRef} from "@heroui/react-utils";
import {clsx, dataAttr, isEmpty, objectToDeps, chain, mergeProps} from "@heroui/shared-utils";
import {clsx, dataAttr, isEmpty, objectToDeps, mergeProps} from "@heroui/shared-utils";
import {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from "react";
import {useToast as useToastAria} from "@react-aria/toast";
import {useHover} from "@react-aria/interactions";
@ -207,6 +207,7 @@ export function useToast<T extends ToastProps>(originalProps: UseToastProps<T>)
const [isLoading, setIsLoading] = useState<boolean>(!!promiseProp);
const [isToastExiting, setIsToastExiting] = useState(false);
const hasCalledOnCloseRef = useRef(false);
useEffect(() => {
if (!promiseProp) return;
@ -224,8 +225,12 @@ export function useToast<T extends ToastProps>(originalProps: UseToastProps<T>)
useEffect(() => {
if (isToastExiting && disableAnimation) {
state.close(toast.key);
if (!hasCalledOnCloseRef.current) {
hasCalledOnCloseRef.current = true;
onClose?.();
}
}
}, [isToastExiting, disableAnimation, state, toast.key]);
}, [isToastExiting, disableAnimation, state, toast.key, onClose]);
useEffect(() => {
const updateProgress = (timestamp: number) => {
@ -436,6 +441,10 @@ export function useToast<T extends ToastProps>(originalProps: UseToastProps<T>)
: () => {
if (!isToastExiting) return;
state.close(toast.key);
if (!hasCalledOnCloseRef.current) {
hasCalledOnCloseRef.current = true;
onClose?.();
}
},
style: {
opacity: opacityValue,
@ -526,14 +535,17 @@ export function useToast<T extends ToastProps>(originalProps: UseToastProps<T>)
"aria-label": "closeButton",
"data-hidden": dataAttr(hideCloseButton),
...mergeProps(props, {
onPress: chain(() => {
onPress: () => {
setIsToastExiting(true);
if (!hasCalledOnCloseRef.current) {
hasCalledOnCloseRef.current = true;
onClose?.();
}
setTimeout(() => document.body.focus(), 0);
}, onClose),
},
}),
}),
[setIsToastExiting, onClose, state, toast],
[setIsToastExiting, onClose],
);
const getCloseIconProps: PropGetter = useCallback(