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; } `;