1 | import { useCallback, useMemo } from 'react';
|
2 | import { useToastContext } from './useToastContext';
|
3 |
|
4 | // By default all toasts are dismissed after a timeout unless specified by the
|
5 | // implementer via `timeout = 0` or `timeout = false`.
|
6 | const DEFAULT_TIMEOUT = 5000;
|
7 |
|
8 | /**
|
9 | * Generates an identifier for a toast by inspecting the properties that
|
10 | * differentiate toasts from one another.
|
11 | *
|
12 | * @typedef getToastId
|
13 | * @kind function
|
14 | *
|
15 | * @param {Object} properties A composite identifier object with properties
|
16 | * that identify a specific toast using its {@link ToastProps}.
|
17 | * @param {String} properties.type Maps to the `type` property of {@link ToastProps}
|
18 | * @param {String} properties.message Maps to the `message` property of {@link ToastProps}
|
19 | * @param {Boolean} properties.dismissable=true Maps to the `dismissable` property of {@link ToastProps}
|
20 | * @param {String} properties.actionText='' Maps to the `actionText` property of {@link ToastProps}
|
21 | * @param {React.Element} properties.icon=()=>{} Maps to the `icon` property of {@link ToastProps}
|
22 | *
|
23 | */
|
24 | export const getToastId = ({
|
25 | type,
|
26 | message,
|
27 | dismissable = true,
|
28 | actionText = '',
|
29 | icon = () => {}
|
30 | }) => {
|
31 | const combined = [type, message, dismissable, actionText, icon].join();
|
32 |
|
33 | // The hashing function below should generally avoid accidental collisions.
|
34 | // It exists to give a "readable" identifier to toasts for debugging.
|
35 | let hash = 0;
|
36 | let i;
|
37 | let chr;
|
38 | if (combined.length === 0) return hash;
|
39 | for (i = 0; i < combined.length; i++) {
|
40 | chr = combined.charCodeAt(i);
|
41 | hash = (hash << 5) - hash + chr;
|
42 | hash |= 0; // Convert to 32bit integer
|
43 | }
|
44 | return hash;
|
45 | };
|
46 |
|
47 | /**
|
48 | * A hook that provides access to the toast state and toast api.
|
49 | *
|
50 | * @kind function
|
51 | *
|
52 | * @returns {Object[]} An array containing objects for the toast state and its API: [{@link ../useToastContext#ToastState ToastState}, {@link API}]
|
53 | */
|
54 | export const useToasts = () => {
|
55 | const [state, dispatch] = useToastContext();
|
56 |
|
57 | /**
|
58 | * Removes a toast from the toast store.
|
59 | *
|
60 | * @function API.removeToast
|
61 | *
|
62 | * @param {Number} id The id of the toast to remove
|
63 | */
|
64 | const removeToast = useCallback(
|
65 | id => {
|
66 | dispatch({
|
67 | type: 'remove',
|
68 | payload: { id }
|
69 | });
|
70 | },
|
71 | [dispatch]
|
72 | );
|
73 |
|
74 | /**
|
75 | * Dispatches an add action. Includes all props passed along with a hash id
|
76 | * and a timeout id generated based on the incoming props.
|
77 | *
|
78 | * @function API.addToast
|
79 | *
|
80 | * @param {ToastProps} toastProps The object containing props for adding a toast.
|
81 | *
|
82 | * @returns {Number} id The key referencing the toast in the store
|
83 | */
|
84 | const addToast = useCallback(
|
85 | /**
|
86 | * Object containing data for creating toasts using {@link API.addToast}.
|
87 | *
|
88 | * @typedef ToastProps
|
89 | *
|
90 | * @property {String} type One of the following toast types: 'info', 'warning',
|
91 | * or 'error'
|
92 | * @property {String} message The message to display on the toast
|
93 | * @property {Bool} [dismissable] Indicates whether the toast is dismissable.
|
94 | * If `onDismiss` is provided, this property is assumed to be true.
|
95 | * This property is optional when creating toasts.
|
96 | * @property {React.Element} [icon] The icon element to display.
|
97 | * This property is optional when creating toasts.
|
98 | * @property {Function} [onDismiss] Callback invoked when a user clicks the
|
99 | * dismiss icon.
|
100 | * This property is optional when creating toasts.
|
101 | * @property {String} [actionText] Text to display as a call to action.
|
102 | * This property is optional when creating toasts.
|
103 | * @property {Function} [onAction] Callback invoked when a user clicks the action
|
104 | * text.
|
105 | * This property is optional when creating toasts.
|
106 | * @property {Number} [timeout] Time, in ms, before the toast is automatically
|
107 | * dismissed.
|
108 | * If `0` or `false` is passed, the toast will not timeout.
|
109 | * This property is optional when creating toasts.
|
110 | *
|
111 | */
|
112 | toastProps => {
|
113 | const {
|
114 | dismissable,
|
115 | message,
|
116 | timeout,
|
117 | type,
|
118 | onDismiss,
|
119 | onAction
|
120 | } = toastProps;
|
121 |
|
122 | if (!type) {
|
123 | throw new TypeError('toast.type is required');
|
124 | }
|
125 |
|
126 | if (!message) {
|
127 | throw new TypeError('toast.message is required');
|
128 | }
|
129 |
|
130 | if (
|
131 | !(timeout || timeout === 0 || timeout === false) &&
|
132 | !(onDismiss || dismissable)
|
133 | ) {
|
134 | throw new TypeError(
|
135 | 'Toast should be user-dismissable or have a timeout'
|
136 | );
|
137 | }
|
138 |
|
139 | // Generate the id to use in the removal timeout.
|
140 | const id = getToastId(toastProps);
|
141 |
|
142 | const handleDismiss = () => {
|
143 | onDismiss ? onDismiss(() => removeToast(id)) : removeToast(id);
|
144 | };
|
145 |
|
146 | const handleAction = () =>
|
147 | onAction ? onAction(() => removeToast(id)) : () => {};
|
148 |
|
149 | // A timeout of 0 means no auto-dismiss.
|
150 | let removalTimeoutId;
|
151 | if (timeout !== 0 && timeout !== false) {
|
152 | removalTimeoutId = setTimeout(
|
153 | () => {
|
154 | handleDismiss();
|
155 | },
|
156 | timeout ? timeout : DEFAULT_TIMEOUT
|
157 | );
|
158 | }
|
159 |
|
160 | dispatch({
|
161 | type: 'add',
|
162 | payload: {
|
163 | ...toastProps,
|
164 | id,
|
165 | timestamp: Date.now(),
|
166 | removalTimeoutId,
|
167 | handleDismiss,
|
168 | handleAction
|
169 | }
|
170 | });
|
171 |
|
172 | return id;
|
173 | },
|
174 | [dispatch, removeToast]
|
175 | );
|
176 |
|
177 | /**
|
178 | * The API for managing toasts.
|
179 | * Use this API to add and remove toasts.
|
180 | *
|
181 | * @typedef API
|
182 | * @type Object
|
183 | */
|
184 | const api = useMemo(
|
185 | () => ({
|
186 | addToast,
|
187 | dispatch,
|
188 | removeToast
|
189 | }),
|
190 | [addToast, dispatch, removeToast]
|
191 | );
|
192 |
|
193 | return [state, api];
|
194 | };
|