/
ToastContext.tsx
123 lines (99 loc) · 2.77 KB
/
ToastContext.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
117
118
119
120
121
122
123
import React, {
useContext,
createContext,
useReducer,
useCallback,
ReactNode,
useState,
useEffect,
Fragment,
} from 'react';
import { createPortal } from 'react-dom';
import { useTheme } from 'sku/react-treat';
import { Toast, Toaster } from './Toaster';
let toastCounter = 0;
type AddToast = (toast: Toast) => void;
const ToastControllerContext = createContext<AddToast | null>(null);
type Actions =
| { type: 'QUEUE_TOAST'; value: Toast }
| { type: 'REMOVE_TOAST'; value: string };
interface ToastState {
toasts: Toast[];
}
function reducer(state: ToastState, action: Actions): ToastState {
switch (action.type) {
case 'QUEUE_TOAST': {
return {
...state,
toasts: [...state.toasts, action.value],
};
}
case 'REMOVE_TOAST': {
const toasts = state.toasts.filter(({ id }) => id !== action.value);
return {
...state,
toasts,
};
}
}
return state;
}
interface ToastProviderProps {
children: ReactNode;
}
export const ToastProvider = ({ children }: ToastProviderProps) => {
const currentContext = useContext(ToastControllerContext);
if (currentContext !== null) {
// Bail early as "ToastProvider" is already setup
return <Fragment>{children}</Fragment>;
}
const [{ toasts }, dispatch] = useReducer(reducer, {
toasts: [],
});
const addToast = useCallback(
(props: Toast) => dispatch({ type: 'QUEUE_TOAST', value: props }),
[],
);
const removeToast = useCallback(
(id: string) => dispatch({ type: 'REMOVE_TOAST', value: id }),
[],
);
return (
<ToastControllerContext.Provider value={addToast}>
{children}
<ToastPortal>
<Toaster toasts={toasts.slice(0, 2)} removeToast={removeToast} />
</ToastPortal>
</ToastControllerContext.Provider>
);
};
interface ToastPortalProps {
children: ReactNode;
}
const ToastPortal = ({ children }: ToastPortalProps) => {
const [toastElement, setElement] = useState<HTMLElement | null>(null);
useEffect(() => {
const toastContainerId = 'braid-toast-container';
let element = document.getElementById(toastContainerId);
if (!element) {
element = document.createElement('div');
element.setAttribute('id', toastContainerId);
element.setAttribute('class', '');
document.body.appendChild(element);
}
setElement(element);
}, []);
if (!toastElement) {
return null;
}
return createPortal(children, toastElement);
};
export const useToast = () => {
const treatTheme = useTheme();
const addToast = useContext(ToastControllerContext);
if (addToast === null) {
throw new Error('No "ToastProvider" configured');
}
return (props: Omit<Toast, 'treatTheme' | 'id'>) =>
addToast({ ...props, treatTheme, id: `${toastCounter++}` });
};