import React, { memo } from 'react';
import styled, { css, keyframes } from 'styled-components';
import type { ReactElement } from 'react';
import type { ToastItem, ToastType } from '@redocly/theme/core/types';
import { CheckmarkFilledIcon } from '@redocly/theme/icons/CheckmarkFilledIcon/CheckmarkFilledIcon';
import { CircleDashIcon } from '@redocly/theme/icons/CircleDashIcon/CircleDashIcon';
import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
import { ErrorFilledIcon } from '@redocly/theme/icons/ErrorFilledIcon/ErrorFilledIcon';
import { InformationFilledIcon } from '@redocly/theme/icons/InformationFilledIcon/InformationFilledIcon';
import { WarningFilledIcon } from '@redocly/theme/icons/WarningFilledIcon/WarningFilledIcon';
import { useToastLogic } from '@redocly/theme/core/hooks';
import { Button } from '@redocly/theme/components/Button/Button';
import { TOAST_SLIDE_DURATION_MS } from '@redocly/theme/core/constants/toast';
function renderToastIcon(type: ToastType): ReactElement {
switch (type) {
case 'success':
return ;
case 'warning':
return ;
case 'error':
return ;
case 'loading':
return ;
case 'info':
default:
return ;
}
}
function renderDismissButton(onClick: () => void): ReactElement {
return (
}
onClick={onClick}
/>
);
}
export interface ToastComponentProps {
toast: ToastItem;
onDismiss: (id: string) => void;
stackIndex: number;
stackZIndex?: number;
className?: string;
}
function ToastComponent({
toast,
onDismiss,
stackIndex,
stackZIndex = 1,
className,
}: ToastComponentProps): ReactElement {
const {
wrapperRef,
hasDetails,
dismissToast,
handleMouseEnter,
handleMouseLeave,
ariaRole,
ariaLive,
} = useToastLogic({
toast,
onDismiss,
stackIndex,
});
const icon = renderToastIcon(toast.type);
const content = !hasDetails ? (
{icon}
{toast.title}
{renderDismissButton(dismissToast)}
) : (
{icon}
{toast.title}
{renderDismissButton(dismissToast)}
{toast.description ? (
{toast.description}
) : null}
);
return (
{content}
);
}
export const Toast = memo(ToastComponent);
const slideIn = keyframes`
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
`;
const slideOut = keyframes`
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(100%);
}
`;
export const spin = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const ToastWrapper = styled.div<{ $stackZIndex: number }>`
position: relative;
z-index: ${({ $stackZIndex }) => $stackZIndex};
width: 100%;
pointer-events: auto;
will-change: transform;
`;
const ToastSurface = styled.div<{ $isExiting: boolean }>`
display: flex;
align-items: flex-start;
width: 100%;
min-width: var(--toast-min-width);
max-width: var(--toast-max-width);
background-color: var(--toast-bg-color);
border: var(--toast-border);
border-radius: var(--toast-border-radius);
box-shadow: var(--toast-box-shadow);
color: var(--toast-text-color);
font-family: var(--toast-font-family);
animation: ${({ $isExiting }) =>
$isExiting
? css`
${slideOut} ${TOAST_SLIDE_DURATION_MS}ms ease-in forwards
`
: css`
${slideIn} ${TOAST_SLIDE_DURATION_MS}ms ease-out
`};
@media (max-width: 480px) {
min-width: 0;
max-width: none;
}
`;
const SimpleToastSurface = styled(ToastSurface)`
gap: var(--toast-simple-gap);
padding: var(--toast-simple-padding);
`;
const DetailedToastSurface = styled(ToastSurface)`
padding: var(--toast-detailed-padding);
`;
const ContentWrapper = styled.div`
display: flex;
flex: 1 1 auto;
gap: var(--toast-content-gap);
min-width: 0;
`;
const IconWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: var(--toast-icon-size);
min-width: var(--toast-icon-size);
height: var(--toast-icon-line-height);
${CircleDashIcon} {
animation: ${spin} var(--toast-loading-animation-duration) linear infinite;
}
`;
const flexItemStyles = css`
flex: 1 1 auto;
min-width: 0;
`;
const SimpleContent = styled.div`
${flexItemStyles}
`;
const Body = styled.div`
display: flex;
${flexItemStyles}
flex-direction: column;
`;
const TitleRow = styled.div`
display: flex;
align-items: center;
gap: var(--toast-title-gap);
min-width: 0;
`;
const DescriptionRow = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--toast-description-gap);
min-width: 0;
`;
const textStyles = css`
margin: 0;
font-size: var(--toast-text-font-size);
line-height: var(--toast-text-line-height);
color: var(--toast-text-color);
overflow-wrap: anywhere;
`;
const TitleText = styled.p`
${textStyles}
${flexItemStyles}
font-weight: var(--toast-title-font-weight);
`;
const SimpleText = styled.p`
${textStyles}
font-weight: var(--toast-body-font-weight);
`;
const DescriptionText = styled.p`
${textStyles}
${flexItemStyles}
font-weight: var(--toast-body-font-weight);
`;
const CloseButton = styled(Button).attrs({
variant: 'ghost',
size: 'small',
})`
flex: 0 0 auto;
min-height: unset;
margin: 0;
padding: var(--toast-close-button-padding);
color: var(--toast-close-button-icon-color);
&:hover,
&:focus-visible {
border: none;
}
`;