import React, { useEffect, useState } from "react"; import toaster from "toasted-notes"; import classNames from "classnames"; import { Action } from "../../types"; import { bemHOF } from "../../utilities/bem"; import { Box } from "../Box"; import { CloseButton } from "../CloseButton"; import { Icon, IconProps, ICON_TYPE } from "../Icon"; import { Link } from "../Link"; import { Text } from "../Text"; const cn = bemHOF("Snackbar"); const POSITION = "bottom-left"; interface SharedProps { /** * The message shown in the Snackbar; ideally short and succinct */ message: string; /** * Adds an action next to the message */ action?: Action; /** * When a loading icon is detected, it will automatically spin. * * Sizing is handled automatically, too. */ icon?: ICON_TYPE; /** * The amount of time (in milliseconds) the snackbar appears on-screen. If * undefined, then the Snackbar will stay on-screen indefinitely. An undefined * duration would be used in cases when the Snackbar is used as a loading * message (e.g., an in progress API call). */ duration?: number | null; /** * Prevent the close button from rendering. Useful in cases when the Snackbar * is used as a loading message (e.g., an in progress API call). */ disableClose?: boolean; } interface ShowSnackbarProps extends SharedProps { /** * Classname to apply to the Snackbar root element */ className?: string; } interface SnackbarProps extends ShowSnackbarProps { onClose: () => void; id?: string; } interface SnackbarHookProps extends SharedProps { /** * Whether or not the Snackar is visible */ isVisible: boolean; } const hideSnackbar = (id: number) => toaster.close(id, POSITION); const SnackbarAction = ({ label, onClick, href, external }: Action) => { const Component = typeof onClick === "function" ? "button" : "a"; const type = Component === "button" ? "button" : undefined; return ( {label} ); }; export const Snackbar = ({ message, action, icon, id, className, onClose, disableClose, }: SnackbarProps) => { const spin = icon === ICON_TYPE.SPINNER || icon === ICON_TYPE.CIRCLE_NOTCH; const iconProps: IconProps | undefined = icon ? { icon, spin, } : undefined; return ( {iconProps && } {message} {action && action.label && ( { if (action.onClick) { action.onClick(); } onClose(); }} /> )} {!disableClose && ( )} ); }; export const showSnackbar = ({ message, action, icon, duration = null, className, disableClose, }: ShowSnackbarProps) => toaster.notify( ({ onClose, id }: { onClose: () => void; id: number }) => { return ( ); }, { position: POSITION, duration, }, ); export const useSnackbar = ({ message, action, isVisible, icon, duration, disableClose, }: SnackbarHookProps) => { const [snackbar, setSnackbar] = useState<{ id: number; position: string }>({ id: 0, position: POSITION, }); const [wasViewed, setWasViewed] = useState(false); useEffect(() => { if (isVisible && !wasViewed) { setSnackbar( showSnackbar({ message, action, icon, duration, disableClose, }), ); setWasViewed(true); } if (!isVisible && wasViewed) { hideSnackbar(snackbar.id); setWasViewed(false); } }, [ message, action, isVisible, icon, duration, disableClose, snackbar, wasViewed, ]); };