UNPKG

6.42 kBJavaScriptView Raw
1import { useCallback, useMemo } from 'react';
2import { 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`.
6const 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 */
24export 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 */
54export 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};