Bladeren bron

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
Connor Prussin 8 maanden geleden
bovenliggende
commit
f5c9bb6dea
2 gewijzigde bestanden met toevoegingen van 97 en 97 verwijderingen
  1. 80 74
      apps/staking/src/components/Root/toast-region.tsx
  2. 17 23
      apps/staking/src/hooks/use-toast.tsx

+ 80 - 74
apps/staking/src/components/Root/toast-region.tsx

@@ -1,94 +1,100 @@
 "use client";
 
 import { XMarkIcon } from "@heroicons/react/24/solid";
-import {
-  type AriaToastRegionProps,
-  type AriaToastProps,
-  useToastRegion,
-  useToast as reactAriaUseToast,
-} from "@react-aria/toast";
 import clsx from "clsx";
-import { useRef, useState } from "react";
-import { Button } from "react-aria-components";
-
+import { AnimatePresence, motion } from "framer-motion";
+import type { ComponentProps } from "react";
+import { useState, useCallback } from "react";
 import {
-  type Toast as ToastContentType,
-  ToastType,
-  useToast,
-} from "../../hooks/use-toast";
+  Button,
+  Text,
+  UNSTABLE_Toast as BaseToast,
+  UNSTABLE_ToastContent as BaseToastContent,
+  UNSTABLE_ToastRegion as BaseToastRegion,
+} from "react-aria-components";
+
+import type { Toast as ToastContentType } from "../../hooks/use-toast";
+import { ToastType, useToast } from "../../hooks/use-toast";
 import { ErrorMessage } from "../ErrorMessage";
 
-export const ToastRegion = (props: AriaToastRegionProps) => {
-  const state = useToast();
-  const ref = useRef(null);
-  const { regionProps } = useToastRegion(props, state, ref);
+const MotionBaseToast = motion(BaseToast);
+
+export const ToastRegion = (
+  props: Omit<
+    ComponentProps<typeof BaseToastRegion<ToastContentType>>,
+    "queue" | "children"
+  >,
+) => {
+  const toast = useToast();
 
   return (
-    <div
-      {...regionProps}
-      ref={ref}
-      className="pointer-events-none fixed top-0 z-50 flex w-full flex-col items-center"
+    <BaseToastRegion
+      className="pointer-events-none fixed top-0 z-50 flex w-full flex-col-reverse items-center"
+      queue={toast.queue}
+      {...props}
     >
-      {state.visibleToasts.map((toast) => (
-        <Toast key={toast.key} toast={toast} />
-      ))}
-    </div>
+      {({ toast }) => <Toast key={toast.key} toast={toast} />}
+    </BaseToastRegion>
   );
 };
 
-const Toast = (props: AriaToastProps<ToastContentType>) => {
+const Toast = (props: ComponentProps<typeof BaseToast<ToastContentType>>) => {
+  const toast = useToast();
+  const [isVisible, setIsVisible] = useState(true);
   const [isTimerStarted, setIsTimerStarted] = useState(false);
-  const state = useToast();
-  const ref = useRef(null);
-  const { toastProps, contentProps, titleProps, closeButtonProps } =
-    reactAriaUseToast(props, state, ref);
+  const hide = useCallback(() => {
+    setIsVisible(false);
+  }, [setIsVisible]);
+  const handlePresenceAnimationComplete = useCallback(
+    (name: string) => {
+      if (name === "exit") {
+        toast.queue.close(props.toast.key);
+      } else {
+        setIsTimerStarted(true);
+      }
+    },
+    [toast, props.toast, setIsTimerStarted],
+  );
 
   return (
-    <div
-      {...toastProps}
-      ref={ref}
-      className="pt-4 data-[entering]:animate-in data-[exiting]:animate-out data-[entering]:slide-in-from-top data-[exiting]:slide-out-to-top"
-      {...((props.toast.animation === "entering" ||
-        props.toast.animation === "queued") && { "data-entering": "" })}
-      {...(props.toast.animation === "exiting" && { "data-exiting": "" })}
-      onAnimationEnd={() => {
-        if (
-          props.toast.animation === "entering" ||
-          props.toast.animation === "queued"
-        ) {
-          setIsTimerStarted(true);
-        }
-        if (props.toast.animation === "exiting") {
-          state.remove(props.toast.key);
-        }
-      }}
-    >
-      <div className="pointer-events-auto w-96 bg-pythpurple-100 text-pythpurple-950">
-        <div
-          className={clsx(
-            "h-1 w-full origin-left bg-green-500 transition-transform [transition-duration:5000ms] [transition-timing-function:linear]",
-            {
-              "scale-x-0": isTimerStarted,
-              "bg-green-500": props.toast.content.type === ToastType.Success,
-              "bg-red-500": props.toast.content.type === ToastType.Error,
-            },
-          )}
-          onTransitionEnd={() => {
-            state.close(props.toast.key);
-          }}
-        />
-        <div className="flex flex-row items-start justify-between gap-8 px-4 py-2">
-          <div {...contentProps}>
-            <div {...titleProps}>
-              <ToastContent>{props.toast.content}</ToastContent>
-            </div>
+    <AnimatePresence>
+      {isVisible && (
+        <MotionBaseToast
+          // @ts-expect-error the framer-motion types don't currently expose
+          // props like `className` correctly for some reason, even though this
+          // works correctly...
+          className="pt-4"
+          initial={{ y: "-100%" }}
+          animate={{ y: 0 }}
+          exit={{ y: "-100%", transition: { ease: "linear", duration: 0.1 } }}
+          onAnimationComplete={handlePresenceAnimationComplete}
+          {...props}
+        >
+          <div className="pointer-events-auto w-96 bg-pythpurple-100 text-pythpurple-950">
+            <div
+              className={clsx(
+                "h-1 w-full origin-left bg-green-500 transition-transform [transition-duration:5000ms] [transition-timing-function:linear]",
+                {
+                  "scale-x-0": isTimerStarted,
+                  "bg-green-500":
+                    props.toast.content.type === ToastType.Success,
+                  "bg-red-500": props.toast.content.type === ToastType.Error,
+                },
+              )}
+              onTransitionEnd={hide}
+            />
+            <BaseToastContent className="flex flex-row items-start justify-between gap-8 px-4 py-2">
+              <Text slot="description">
+                <ToastContent>{props.toast.content}</ToastContent>
+              </Text>
+              <Button onPress={hide}>
+                <XMarkIcon className="mt-1 size-4" />
+              </Button>
+            </BaseToastContent>
           </div>
-          <Button {...closeButtonProps}>
-            <XMarkIcon className="mt-1 size-4" />
-          </Button>
-        </div>
-      </div>
-    </div>
+        </MotionBaseToast>
+      )}
+    </AnimatePresence>
   );
 };
 

+ 17 - 23
apps/staking/src/hooks/use-toast.tsx

@@ -1,16 +1,8 @@
 "use client";
 
-import {
-  type ToastState as BaseToastState,
-  useToastState,
-} from "@react-stately/toast";
-import {
-  type ComponentProps,
-  type ReactNode,
-  createContext,
-  useContext,
-  useCallback,
-} from "react";
+import type { ComponentProps, ReactNode } from "react";
+import { createContext, useContext, useCallback, useMemo } from "react";
+import { UNSTABLE_ToastQueue as ToastQueue } from "react-aria-components";
 
 export enum ToastType {
   Success,
@@ -25,7 +17,8 @@ const Toast = {
 };
 export type Toast = ReturnType<(typeof Toast)[keyof typeof Toast]>;
 
-type ToastState = BaseToastState<Toast> & {
+type ToastState = {
+  queue: ToastQueue<Toast>;
   success: (message: ReactNode) => void;
   error: (error: unknown) => void;
 };
@@ -38,23 +31,24 @@ type ToastContextProps = Omit<
 >;
 
 export const ToastProvider = (props: ToastContextProps) => {
-  const toast = useToastState<Toast>({
-    maxVisibleToasts: 3,
-    hasExitAnimation: true,
-  });
+  const queue = useMemo(
+    () =>
+      new ToastQueue<Toast>({
+        maxVisibleToasts: 3,
+      }),
+    [],
+  );
 
   const success = useCallback(
-    (message: ReactNode) => toast.add(Toast.Success(message)),
-    [toast],
+    (message: ReactNode) => queue.add(Toast.Success(message)),
+    [queue],
   );
   const error = useCallback(
-    (error: unknown) => toast.add(Toast.ErrorToast(error)),
-    [toast],
+    (error: unknown) => queue.add(Toast.ErrorToast(error)),
+    [queue],
   );
 
-  return (
-    <ToastContext.Provider value={{ ...toast, success, error }} {...props} />
-  );
+  return <ToastContext.Provider value={{ queue, success, error }} {...props} />;
 };
 
 export const useToast = () => {