-
Notifications
You must be signed in to change notification settings - Fork 2
/
toaster.tsx
116 lines (103 loc) · 3.94 KB
/
toaster.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import { $, component$, createContextId, useContext, useContextProvider, useId, useSignal, useStyles$, useTask$ } from "@builder.io/qwik";
import type { JSXOutput, QRL} from "@builder.io/qwik";
import type { UlAttributes } from "../types";
import { clsq, cssvar } from '../utils';
import styles from './toaster.scss?inline';
export interface ToastProps {
id: string;
duration: number;
position: 'start' | 'center' | 'end';
content: string | QRL<(props: ToastProps) => JSXOutput>;
/**
* Aria role: default 'status'.
* @description The toast uses the html element output, which has implicit role 'status'
* For information that requires an immediate attention of the user use 'alert'.
*/
role: 'alert' | 'status';
class?: string;
}
export type ToastParams = Partial<Omit<ToastProps, 'content'>>;
export type ToastNode = QRL<(props: ToastProps) => JSXOutput>;
export const ToasterContext = createContextId<ToasterService>('ToasterContext');
type ToasterService = ReturnType<typeof useToasterProvider>;
export const useToasterProvider = () => {
const toaster = useSignal<HTMLElement>();
const toasts = useSignal<ToastProps[]>([]);
const service = {
toaster,
toasts,
add: $((content: ToastProps['content'], params: ToastParams = {}) => {
params.id ||= crypto.randomUUID();
params.duration ||= 1500;
params.position ||= 'center';
params.role ||= 'status';
toastFlip(toaster.value);
toasts.value = toasts.value.concat({ content, ...params } as ToastProps);
}),
remove: $((id: string) => {
const index = toasts.value.findIndex(t => t.id === id);
toasts.value[index].duration = 0;
toasts.value = [...toasts.value];
}),
}
useContextProvider(ToasterContext, service);
// TODO: remove after useContext is fixed in v2.0
useContext(ToasterContext);
return service;
}
function toastFlip(toaster?: HTMLElement) {
if (!toaster) return;
const previous: Record<string, number> = {};
const list = toaster.querySelectorAll('li');
for (const item of list) {
previous[item.id] = item.getBoundingClientRect().top;
}
const animate = () => {
const newList = toaster.querySelectorAll('li');
if (newList.length === list.length) requestAnimationFrame(animate);
for (const item of newList) {
const delta = previous[item.id] - item.getBoundingClientRect().top;
if (delta) {
item.animate({ transform: [`translateY(${delta}px)`, `translateY(0)`] }, {
duration: 150,
easing: 'ease-out'
});
}
}
}
requestAnimationFrame(animate)
}
export const useToaster = () => useContext(ToasterContext);
export const Toaster = component$((props: UlAttributes) => {
useStyles$(styles);
const { toaster, toasts } = useContext(ToasterContext);
return <ul {...props} ref={toaster} class={clsq('toaster', props.class)}>
{toasts.value.map((props) => <Toast key={props.id} {...props} />)}
</ul>
});
export const Toast = component$((props: ToastProps) => {
const { toaster, toasts } = useContext(ToasterContext);
const { content, position, ...attributes } = props;
const ref = useSignal<HTMLElement>();
const itemId = useId();
const leaving = useSignal(false);
const toastPosition = position === 'center' ? position : `flex-${position}`;
useTask$(({ track }) => {
track(() => props.duration);
const leave = () => {
leaving.value = true;
ref.value?.addEventListener('animationend', () => {
toastFlip(toaster.value);
toasts.value = toasts.value.filter(t => t.id !== props.id);
}, { once: true });
};
if (!props.duration) return leave();
const timeout = setTimeout(leave, props.duration);
return () => clearTimeout(timeout);
})
return <li ref={ref} id={itemId} class={leaving.value ? 'leave' : ''} {...cssvar({toastPosition})}>
<output {...attributes} class={clsq('toast', props.class)}>
{typeof content === 'string' ? content : content(props)}
</output>
</li>
});