✨(react) add Toast component
This component allows to create dynamic Toast appearing at the bottom of the screen for few seconds.
This commit is contained in:
120
packages/react/src/components/Toast/index.tsx
Normal file
120
packages/react/src/components/Toast/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user