Files
cunningham/packages/react/src/components/Toast/index.tsx
Nathan Vasse 87dbf922df 🚨(react) fix lint issues
Fix the lint for the recent commits.
2025-09-23 15:58:43 +02:00

124 lines
3.1 KiB
TypeScript

import React, {
PropsWithChildren,
ReactNode,
useEffect,
useMemo,
useRef,
} from "react";
import classNames from "classnames";
import isChromatic from "chromatic/isChromatic";
import { Button, ButtonProps } from ":/components/Button";
import { iconFromType, VariantType } from ":/utils/VariantUtils";
export interface ToastProps extends PropsWithChildren {
duration: number;
type: VariantType;
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>(null);
// Register a timeout to remove the toast after the duration.
useEffect(() => {
if (props.disableAnimate) {
return;
}
disappearTimeout.current = setTimeout(async () => {
setAnimateDisappear(true);
disappearTimeout.current = null;
}, 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
variant="primary"
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(() => {
if (isChromatic()) {
return;
}
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>
);
};