Files
cunningham/packages/react/src/components/Toast/index.tsx
Nathan Vasse 132b676ff7 (react) add Toast component
This component allows to create dynamic Toast appearing at the bottom
of the screen for few seconds.
2024-01-05 16:38:09 +01:00

121 lines
3.1 KiB
TypeScript

import React, {
PropsWithChildren,
ReactNode,
useEffect,
useMemo,
useRef,
} from "react";
import classNames from "classnames";
import { ToastType } from ":/components/Toast/ToastProvider";
import { iconFromType } from ":/components/Alert/Utils";
import { Button, ButtonProps } from ":/components/Button";
export interface ToastProps extends PropsWithChildren {
duration: number;
type: ToastType;
onDelete?: () => void;
icon?: ReactNode;
primaryLabel?: string;
primaryOnClick?: ButtonProps["onClick"];
primaryProps?: ButtonProps;
disableAnimate?: boolean;
actions?: ReactNode;
}
export const Toast = (props: ToastProps) => {
const [animateDisappear, setAnimateDisappear] = React.useState(false);
const container = useRef<HTMLDivElement>(null);
const disappearTimeout = useRef<NodeJS.Timeout>();
// Register a timeout to remove the toast after the duration.
useEffect(() => {
if (props.disableAnimate) {
return;
}
disappearTimeout.current = setTimeout(async () => {
setAnimateDisappear(true);
disappearTimeout.current = undefined;
}, props.duration);
return () => {
if (disappearTimeout.current) {
clearTimeout(disappearTimeout.current);
}
};
}, []);
const removeAfterAnimation = async () => {
await Promise.allSettled(
container.current!.getAnimations().map((animation) => animation.finished),
);
props.onDelete?.();
};
// Remove the toast after the animation finishes.
useEffect(() => {
if (animateDisappear) {
removeAfterAnimation();
}
}, [animateDisappear]);
return (
<div
ref={container}
className={classNames("c__toast", "c__toast--" + props.type, {
"c__toast--disappear": animateDisappear,
"c__toast--no-animate": props.disableAnimate,
})}
role="alert"
>
<ProgressBar duration={props.duration} />
<div className="c__toast__content">
{props.primaryLabel && (
<div className="c__toast__content__buttons">
<Button
color="primary-text"
onClick={props.primaryOnClick}
{...props.primaryProps}
>
{props.primaryLabel}
</Button>
</div>
)}
{props.actions}
<div className="c__toast__content__children">{props.children}</div>
<ToastIcon {...props} />
</div>
</div>
);
};
export const ToastIcon = ({ type, ...props }: ToastProps) => {
const icon = useMemo(() => iconFromType(type), [type]);
if (props.icon) {
return props.icon;
}
if (!icon) {
return null;
}
return (
<div className="c__toast__icon">
<span className="material-icons">{icon}</span>
</div>
);
};
export const ProgressBar = ({ duration }: { duration: number }) => {
const content = useRef<HTMLDivElement>(null);
useEffect(() => {
content.current!.animate([{ width: "0%" }, { width: "100%" }], {
duration,
easing: "linear",
});
}, []);
return (
<div className="c__progress-bar">
<div className="c__progress-bar__content" ref={content} />
</div>
);
};