Files
standardnotes-app-web/packages/toast/src/Toast.tsx
2023-07-13 15:21:28 +05:30

136 lines
4.5 KiB
TypeScript

import type { Toast as ToastPropType } from './types'
import { CheckCircleFilledIcon, ClearCircleFilledIcon } from '@standardnotes/icons'
import { dismissToast } from './toastStore'
import { ToastType } from './enums'
import { ForwardedRef, forwardRef, RefObject, useEffect } from 'react'
const prefersReducedMotion = () => {
const mediaQuery = matchMedia('(prefers-reduced-motion: reduce)')
return mediaQuery.matches
}
const colorForToastType = (type: ToastType) => {
switch (type) {
case ToastType.Success:
return 'bg-success text-info-contrast md:text-success'
case ToastType.Error:
return 'bg-danger text-info-contrast md:text-danger'
default:
return 'bg-info text-info-contrast md:text-info'
}
}
const iconForToastType = (type: ToastType) => {
switch (type) {
case ToastType.Success:
return <CheckCircleFilledIcon className="text-success h-5 w-5" />
case ToastType.Error:
return <ClearCircleFilledIcon className="text-danger h-5 w-5" />
case ToastType.Progress:
case ToastType.Loading:
return <div className="border-info h-4 w-4 animate-spin rounded-full border border-solid border-r-transparent" />
default:
return null
}
}
type Props = {
toast: ToastPropType
index: number
}
export const Toast = forwardRef(({ toast, index }: Props, ref: ForwardedRef<HTMLDivElement>) => {
const icon = iconForToastType(toast.type)
const hasActions = toast.actions && toast.actions.length > 0
const hasProgress = toast.type === ToastType.Progress && toast.progress !== undefined && toast.progress > -1
const shouldReduceMotion = prefersReducedMotion()
const enterAnimation = shouldReduceMotion ? 'fade-in-animation' : 'slide-in-right-animation'
const exitAnimation = shouldReduceMotion ? 'fade-out-animation' : 'slide-out-left-animation'
const currentAnimation = toast.dismissed ? exitAnimation : enterAnimation
useEffect(() => {
if (!ref) {
return
}
const element = (ref as RefObject<HTMLDivElement>).current
if (element && toast.dismissed) {
const { scrollHeight, style } = element
requestAnimationFrame(() => {
style.minHeight = 'initial'
style.height = scrollHeight + 'px'
style.transition = 'all 200ms'
requestAnimationFrame(() => {
style.height = '0'
style.padding = '0'
style.margin = '0'
})
})
}
}, [ref, toast.dismissed])
return (
<div
data-index={index}
role="status"
className={`bg-passive-5 animation-fill-forwards relative mt-3 flex min-w-full select-none flex-col rounded opacity-0 md:min-w-max ${currentAnimation}`}
style={{
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.16)',
transition: shouldReduceMotion ? undefined : 'all 0.2s ease',
animationDelay: !toast.dismissed ? '50ms' : undefined,
}}
onClick={() => {
if (toast.type !== ToastType.Loading && toast.type !== ToastType.Progress) {
dismissToast(toast.id)
}
}}
ref={ref}
>
<div
className={`flex w-full flex-wrap items-center gap-2 ${
hasActions ? 'p-2 pl-3' : hasProgress ? 'px-3 py-2.5' : 'p-3'
}`}
>
{icon ? <div className="sn-icon flex flex-shrink-0 items-center justify-center">{icon}</div> : null}
<div className="text-text text-sm [word-wrap:anywhere]">{toast.message}</div>
{hasActions && (
<>
{toast.actions?.map((action, index) => (
<button
className={`hover:bg-passive-3 cursor-pointer rounded border-0 px-[0.45rem] py-1 text-sm font-semibold md:bg-transparent ${colorForToastType(
toast.type,
)} ${index !== 0 ? 'ml-2' : ''}`}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
action.handler(toast.id)
}}
key={index}
>
{action.label}
</button>
))}
</>
)}
</div>
{hasProgress && (
<div className="bg-default w-full overflow-hidden rounded rounded-tl-none rounded-tr-none">
<div
className="bg-info h-2 rounded rounded-tl-none transition-[width] duration-100"
role="progressbar"
style={{
width: `${toast.progress}%`,
...(toast.progress === 100 ? { borderTopRightRadius: 0 } : {}),
}}
aria-valuenow={toast.progress}
/>
</div>
)}
</div>
)
})