main
ts 82 lines 2.53 KB
Raw
1 import {
2 createElement as h, HTMLAttributes, isValidElement, ReactElement, useEffect, useState, useRef, useCallback
3 } from 'react'
4 import { proxy, ref, useSnapshot } from 'valtio'
5 import { AlertType } from './dialog'
6 import { hIcon, pendingPromise } from './misc'
7 import _ from 'lodash'
8 import './toasts.scss'
9
10 type ToastType = AlertType | 'success'
11 type Content = string | ReactElement
12
13 export function toast(content: Content, type: ToastType='info', { timeout=5_000 }: { timeout?: number } & Omit<ToastOptions, 'id' | 'content' | 'type'>={}) {
14 console.debug("toast", content)
15 const id = Math.random()
16 toasts.push({
17 id,
18 // keep react elements out of valtio proxy snapshots
19 content: isValidElement(content) ? ref(content) : content,
20 type: isValidElement(type) ? ref(type) : type,
21 close
22 })
23 const closed = pendingPromise()
24 setTimeout(close, timeout)
25 return {
26 close,
27 closed
28 }
29
30 function close() {
31 const it = _.find(toasts, { id })
32 if (!it) return
33 it.closed = true
34 closed.resolve()
35 }
36 }
37
38 interface ToastOptions extends Omit<Partial<HTMLAttributes<HTMLDivElement>>, 'id' | 'content'> {
39 content: Content
40 type: ToastType
41 }
42 interface ToastRecord extends ToastOptions {
43 id: number
44 closed?: boolean
45 close: () => void
46 }
47 const toasts = proxy<ToastRecord[]>([])
48
49 export function Toasts() {
50 const snap = useSnapshot(toasts)
51 return h('div', { className: 'toasts' },
52 snap.map(d =>
53 h(Toast, { key: d.id, ...(d as any) }))
54 )
55 }
56
57 function Toast({ content, type, closed, id, close, ...props }: ToastRecord) {
58 const [addClass, setAddClass] = useState('before')
59 const [height, setHeight] = useState('')
60 useEffect(() => {
61 setAddClass(closed ? 'after' : '')
62 }, [closed])
63 const onTransitionEnd = useCallback(() => {
64 const el = ref.current
65 if (!addClass && el) // just entered
66 setHeight(el.clientHeight + 'px')
67 if (addClass === 'after')
68 _.remove(toasts, { id })
69 }, [addClass])
70 const ref = useRef<HTMLDivElement | null>()
71 return h('div', {
72 ...props,
73 ref,
74 style: { height },
75 onTransitionEnd,
76 onClick: close,
77 className: `toast ${addClass} ${_.isString(type) ? 'toast-' + type : ''} ${props.className || ''}`
78 },
79 h('div', { className: 'toast-icon' }, isValidElement(type) ? type : hIcon(type)),
80 h('div', { className: 'toast-content' }, content)
81 )
82 }