Skip to content

Commit f5c9bb6

Browse files
committed
chore: rewrite staking toasts
The upgraded react-aria library comes with much upgraded Toast apis, so this commit moves us to take advantage of those
1 parent c3e373e commit f5c9bb6

File tree

2 files changed

+97
-97
lines changed

2 files changed

+97
-97
lines changed

apps/staking/src/components/Root/toast-region.tsx

Lines changed: 80 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,100 @@
11
"use client";
22

33
import { XMarkIcon } from "@heroicons/react/24/solid";
4-
import {
5-
type AriaToastRegionProps,
6-
type AriaToastProps,
7-
useToastRegion,
8-
useToast as reactAriaUseToast,
9-
} from "@react-aria/toast";
104
import clsx from "clsx";
11-
import { useRef, useState } from "react";
12-
import { Button } from "react-aria-components";
13-
5+
import { AnimatePresence, motion } from "framer-motion";
6+
import type { ComponentProps } from "react";
7+
import { useState, useCallback } from "react";
148
import {
15-
type Toast as ToastContentType,
16-
ToastType,
17-
useToast,
18-
} from "../../hooks/use-toast";
9+
Button,
10+
Text,
11+
UNSTABLE_Toast as BaseToast,
12+
UNSTABLE_ToastContent as BaseToastContent,
13+
UNSTABLE_ToastRegion as BaseToastRegion,
14+
} from "react-aria-components";
15+
16+
import type { Toast as ToastContentType } from "../../hooks/use-toast";
17+
import { ToastType, useToast } from "../../hooks/use-toast";
1918
import { ErrorMessage } from "../ErrorMessage";
2019

21-
export const ToastRegion = (props: AriaToastRegionProps) => {
22-
const state = useToast();
23-
const ref = useRef(null);
24-
const { regionProps } = useToastRegion(props, state, ref);
20+
const MotionBaseToast = motion(BaseToast);
21+
22+
export const ToastRegion = (
23+
props: Omit<
24+
ComponentProps<typeof BaseToastRegion<ToastContentType>>,
25+
"queue" | "children"
26+
>,
27+
) => {
28+
const toast = useToast();
2529

2630
return (
27-
<div
28-
{...regionProps}
29-
ref={ref}
30-
className="pointer-events-none fixed top-0 z-50 flex w-full flex-col items-center"
31+
<BaseToastRegion
32+
className="pointer-events-none fixed top-0 z-50 flex w-full flex-col-reverse items-center"
33+
queue={toast.queue}
34+
{...props}
3135
>
32-
{state.visibleToasts.map((toast) => (
33-
<Toast key={toast.key} toast={toast} />
34-
))}
35-
</div>
36+
{({ toast }) => <Toast key={toast.key} toast={toast} />}
37+
</BaseToastRegion>
3638
);
3739
};
3840

39-
const Toast = (props: AriaToastProps<ToastContentType>) => {
41+
const Toast = (props: ComponentProps<typeof BaseToast<ToastContentType>>) => {
42+
const toast = useToast();
43+
const [isVisible, setIsVisible] = useState(true);
4044
const [isTimerStarted, setIsTimerStarted] = useState(false);
41-
const state = useToast();
42-
const ref = useRef(null);
43-
const { toastProps, contentProps, titleProps, closeButtonProps } =
44-
reactAriaUseToast(props, state, ref);
45+
const hide = useCallback(() => {
46+
setIsVisible(false);
47+
}, [setIsVisible]);
48+
const handlePresenceAnimationComplete = useCallback(
49+
(name: string) => {
50+
if (name === "exit") {
51+
toast.queue.close(props.toast.key);
52+
} else {
53+
setIsTimerStarted(true);
54+
}
55+
},
56+
[toast, props.toast, setIsTimerStarted],
57+
);
4558

4659
return (
47-
<div
48-
{...toastProps}
49-
ref={ref}
50-
className="pt-4 data-[entering]:animate-in data-[exiting]:animate-out data-[entering]:slide-in-from-top data-[exiting]:slide-out-to-top"
51-
{...((props.toast.animation === "entering" ||
52-
props.toast.animation === "queued") && { "data-entering": "" })}
53-
{...(props.toast.animation === "exiting" && { "data-exiting": "" })}
54-
onAnimationEnd={() => {
55-
if (
56-
props.toast.animation === "entering" ||
57-
props.toast.animation === "queued"
58-
) {
59-
setIsTimerStarted(true);
60-
}
61-
if (props.toast.animation === "exiting") {
62-
state.remove(props.toast.key);
63-
}
64-
}}
65-
>
66-
<div className="pointer-events-auto w-96 bg-pythpurple-100 text-pythpurple-950">
67-
<div
68-
className={clsx(
69-
"h-1 w-full origin-left bg-green-500 transition-transform [transition-duration:5000ms] [transition-timing-function:linear]",
70-
{
71-
"scale-x-0": isTimerStarted,
72-
"bg-green-500": props.toast.content.type === ToastType.Success,
73-
"bg-red-500": props.toast.content.type === ToastType.Error,
74-
},
75-
)}
76-
onTransitionEnd={() => {
77-
state.close(props.toast.key);
78-
}}
79-
/>
80-
<div className="flex flex-row items-start justify-between gap-8 px-4 py-2">
81-
<div {...contentProps}>
82-
<div {...titleProps}>
83-
<ToastContent>{props.toast.content}</ToastContent>
84-
</div>
60+
<AnimatePresence>
61+
{isVisible && (
62+
<MotionBaseToast
63+
// @ts-expect-error the framer-motion types don't currently expose
64+
// props like `className` correctly for some reason, even though this
65+
// works correctly...
66+
className="pt-4"
67+
initial={{ y: "-100%" }}
68+
animate={{ y: 0 }}
69+
exit={{ y: "-100%", transition: { ease: "linear", duration: 0.1 } }}
70+
onAnimationComplete={handlePresenceAnimationComplete}
71+
{...props}
72+
>
73+
<div className="pointer-events-auto w-96 bg-pythpurple-100 text-pythpurple-950">
74+
<div
75+
className={clsx(
76+
"h-1 w-full origin-left bg-green-500 transition-transform [transition-duration:5000ms] [transition-timing-function:linear]",
77+
{
78+
"scale-x-0": isTimerStarted,
79+
"bg-green-500":
80+
props.toast.content.type === ToastType.Success,
81+
"bg-red-500": props.toast.content.type === ToastType.Error,
82+
},
83+
)}
84+
onTransitionEnd={hide}
85+
/>
86+
<BaseToastContent className="flex flex-row items-start justify-between gap-8 px-4 py-2">
87+
<Text slot="description">
88+
<ToastContent>{props.toast.content}</ToastContent>
89+
</Text>
90+
<Button onPress={hide}>
91+
<XMarkIcon className="mt-1 size-4" />
92+
</Button>
93+
</BaseToastContent>
8594
</div>
86-
<Button {...closeButtonProps}>
87-
<XMarkIcon className="mt-1 size-4" />
88-
</Button>
89-
</div>
90-
</div>
91-
</div>
95+
</MotionBaseToast>
96+
)}
97+
</AnimatePresence>
9298
);
9399
};
94100

apps/staking/src/hooks/use-toast.tsx

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
11
"use client";
22

3-
import {
4-
type ToastState as BaseToastState,
5-
useToastState,
6-
} from "@react-stately/toast";
7-
import {
8-
type ComponentProps,
9-
type ReactNode,
10-
createContext,
11-
useContext,
12-
useCallback,
13-
} from "react";
3+
import type { ComponentProps, ReactNode } from "react";
4+
import { createContext, useContext, useCallback, useMemo } from "react";
5+
import { UNSTABLE_ToastQueue as ToastQueue } from "react-aria-components";
146

157
export enum ToastType {
168
Success,
@@ -25,7 +17,8 @@ const Toast = {
2517
};
2618
export type Toast = ReturnType<(typeof Toast)[keyof typeof Toast]>;
2719

28-
type ToastState = BaseToastState<Toast> & {
20+
type ToastState = {
21+
queue: ToastQueue<Toast>;
2922
success: (message: ReactNode) => void;
3023
error: (error: unknown) => void;
3124
};
@@ -38,23 +31,24 @@ type ToastContextProps = Omit<
3831
>;
3932

4033
export const ToastProvider = (props: ToastContextProps) => {
41-
const toast = useToastState<Toast>({
42-
maxVisibleToasts: 3,
43-
hasExitAnimation: true,
44-
});
34+
const queue = useMemo(
35+
() =>
36+
new ToastQueue<Toast>({
37+
maxVisibleToasts: 3,
38+
}),
39+
[],
40+
);
4541

4642
const success = useCallback(
47-
(message: ReactNode) => toast.add(Toast.Success(message)),
48-
[toast],
43+
(message: ReactNode) => queue.add(Toast.Success(message)),
44+
[queue],
4945
);
5046
const error = useCallback(
51-
(error: unknown) => toast.add(Toast.ErrorToast(error)),
52-
[toast],
47+
(error: unknown) => queue.add(Toast.ErrorToast(error)),
48+
[queue],
5349
);
5450

55-
return (
56-
<ToastContext.Provider value={{ ...toast, success, error }} {...props} />
57-
);
51+
return <ToastContext.Provider value={{ queue, success, error }} {...props} />;
5852
};
5953

6054
export const useToast = () => {

0 commit comments

Comments
 (0)