Skip to content

Commit 5b775df

Browse files
committed
feat: Add progress bars to expiring toasts
1 parent f3a89f2 commit 5b775df

File tree

4 files changed

+81
-2
lines changed

4 files changed

+81
-2
lines changed

app/src/components/toast/Toast.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Button } from "@phoenix/components/button";
1111
import { Text } from "@phoenix/components/content";
1212
import { Icon, Icons } from "@phoenix/components/icon";
1313
import { toastCss, toastRegionCss } from "@phoenix/components/toast/styles";
14+
import { useTimeoutRemainingPercentage } from "@phoenix/components/toast/useTimeoutRemainingPercentage";
1415
import { NotificationParams, useTheme } from "@phoenix/contexts";
1516

1617
export const ToastRegion = <Q extends AriaToastQueue<NotificationParams>>({
@@ -54,15 +55,20 @@ export const Toast = <
5455
toast: T;
5556
queue?: Q;
5657
}) => {
58+
const { timePercentageRemaining, pauseTimer, unpauseTimer } =
59+
useTimeoutRemainingPercentage(toast.timeout);
5760
const { theme } = useTheme();
5861
return (
5962
<AriaToast
6063
toast={toast}
6164
css={toastCss}
6265
className="react-aria-Toast"
66+
onPointerEnter={pauseTimer}
67+
onPointerLeave={unpauseTimer}
6368
style={{
6469
viewTransitionName: toast.key,
65-
// @ts-expect-error incorrect react types
70+
// @ts-expect-error css vars are not typed properly by react
71+
"--toast-timeout-percent": timePercentageRemaining,
6672
"--ac-internal-token-color": colorFromVariant(toast.content.variant),
6773
}}
6874
data-variant={toast.content.variant}

app/src/components/toast/styles.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,21 @@ export const toastCss = css`
5252
background-color: var(--toast-background-color);
5353
border: var(--toast-border);
5454
color: var(--toast-color);
55+
position: relative;
56+
overflow: hidden;
57+
58+
&:after {
59+
transition: width 500ms linear;
60+
content: "";
61+
position: absolute;
62+
top: -1px;
63+
left: 0;
64+
background: var(--toast-color);
65+
border-radius: 2px 0 2px 0;
66+
border-top: var(--toast-border);
67+
height: 1px;
68+
width: calc(var(--toast-timeout-percent) * 1%);
69+
}
5570
5671
&[data-focus-visible] {
5772
outline: 2px solid slateblue;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useCallback, useEffect, useRef, useState } from "react";
2+
3+
const INTERVAL_MS = 250;
4+
5+
export const useTimeoutRemainingPercentage = (
6+
timeout: number | undefined | null
7+
) => {
8+
const [pauseTimer, setPauseTimer] = useState(false);
9+
const [timeRemaining, setTimeRemaining] = useState<number | null>(() => {
10+
if (timeout) {
11+
return Math.max(timeout - INTERVAL_MS * 2, 0);
12+
}
13+
14+
return null;
15+
});
16+
const initialTimeRemaining = useRef(timeRemaining);
17+
// update initial time remaining when the toast timeout definition changes
18+
useEffect(() => {
19+
if (timeout) {
20+
setTimeRemaining(Math.max(timeout - INTERVAL_MS * 2, 0));
21+
}
22+
}, [timeout]);
23+
// count down from timeRemaining, if set by the toast timeout
24+
useEffect(() => {
25+
if (initialTimeRemaining.current === null) return;
26+
if (pauseTimer) {
27+
return;
28+
}
29+
const interval = setInterval(() => {
30+
setTimeRemaining((prev) => {
31+
if (prev === null) return null;
32+
if (prev <= INTERVAL_MS) {
33+
clearInterval(interval);
34+
return 0;
35+
}
36+
return prev - INTERVAL_MS;
37+
});
38+
}, INTERVAL_MS);
39+
return () => clearInterval(interval);
40+
}, [pauseTimer]);
41+
42+
const timePercentageRemaining =
43+
timeRemaining !== null ? (timeRemaining / (timeout || 1)) * 100 : undefined;
44+
45+
const pauseTimerCallback = useCallback(() => {
46+
setPauseTimer(true);
47+
}, []);
48+
49+
const unpauseTimer = useCallback(() => {
50+
setPauseTimer(false);
51+
}, []);
52+
53+
return {
54+
timePercentageRemaining,
55+
pauseTimer: pauseTimerCallback,
56+
unpauseTimer,
57+
};
58+
};

app/stories/ToastRegion.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const TriggerToasts = () => {
6565
notifySuccess({
6666
title: "Expiring Toast",
6767
message: "This toast will expire soon.",
68-
expireMs: 3000,
68+
expireMs: 5_000,
6969
})
7070
}
7171
>

0 commit comments

Comments
 (0)