Skip to content

Commit 4aed2e2

Browse files
committed
feat(toasts): change toast interface
BREAKING CHANGE: Toasts public interface relied on useContext hook, now it has its own hook interface: ```javascript import { useToasts } from 'react-bootstrap-utils'; function Component() { const { showToast, closeAllToasts } = useToasts(); //... } ```
1 parent cc5a54a commit 4aed2e2

9 files changed

+154
-69
lines changed

demo/ToastsExamples.jsx

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import React, { useContext } from 'react';
2-
import { ToastsContext } from '../dist/main';
1+
import React from 'react';
2+
import { useToasts } from '../dist/main';
33

44
export function ToastsExamples() {
5-
const toastsState = useContext(ToastsContext);
5+
const { showToast, closeAllToasts } = useToasts();
66

77
return (
88
<div className="row">
99
<div className="col-6 mb-5">
1010
<h1 className="h4">Top left </h1>
1111
<button
12+
type="button"
1213
className="btn btn-info"
1314
onClick={() => {
14-
toastsState.show('Top left toast!', {
15+
showToast('Top left toast!', {
1516
position: 'TOP_LEFT',
1617
});
1718
}}
@@ -22,9 +23,10 @@ export function ToastsExamples() {
2223
<div className="col-6 mb-5">
2324
<h1 className="h4">Top right (default)</h1>
2425
<button
26+
type="button"
2527
className="btn btn-success"
2628
onClick={() => {
27-
toastsState.show('Top right toast!', { autoClose: false, type: 'success' });
29+
showToast('Top right toast!', { autoClose: false, type: 'success' });
2830
}}
2931
>
3032
Show top right toast
@@ -33,9 +35,10 @@ export function ToastsExamples() {
3335
<div className="col-6 mb-5">
3436
<h1 className="h4">Bottom left </h1>
3537
<button
38+
type="button"
3639
className="btn btn-danger"
3740
onClick={() => {
38-
toastsState.show('Bottom left toast!', {
41+
showToast('Bottom left toast!', {
3942
position: 'BOTTOM_LEFT',
4043
type: 'danger',
4144
});
@@ -47,9 +50,10 @@ export function ToastsExamples() {
4750
<div className="col-6 mb-5">
4851
<h1 className="h4">Bottom right </h1>
4952
<button
53+
type="button"
5054
className="btn btn-warning"
5155
onClick={() => {
52-
toastsState.show('Bottom right toast!', {
56+
showToast('Bottom right toast!', {
5357
position: 'BOTTOM_RIGHT',
5458
type: 'warning',
5559
});
@@ -58,6 +62,11 @@ export function ToastsExamples() {
5862
Show bottom right toast
5963
</button>
6064
</div>
65+
<div className="col-12">
66+
<button type="button" className="btn btn-secondary" onClick={closeAllToasts}>
67+
Close all
68+
</button>
69+
</div>
6170
</div>
6271
);
6372
}

src/toasts/ToastsContainer.jsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3-
import { ToastsContext, toastPositions } from './toasts-helpers';
3+
import { ToastsContext, TOASTS_CLASSNAME_BY_POSITION } from './toasts-helpers';
44
import { ToastsRegion } from './ToastsRegion';
5-
import { useToast } from './useToast';
5+
import { useToastState } from './useToastState';
66

77
export function ToastsContainer({ children, unique, noStyle }) {
8-
const toastsState = useToast({ unique });
8+
const toastsState = useToastState({ unique });
99

1010
return (
1111
<ToastsContext.Provider value={toastsState}>
1212
{children}
1313

1414
<div className="toast-container">
15-
{toastPositions.map(({ name, className }) => (
15+
{TOASTS_CLASSNAME_BY_POSITION.map(({ name, className }) => (
1616
<ToastsRegion key={name} name={name} className={className} toasts={toastsState.get(name)} noStyle={noStyle} />
1717
))}
1818
</div>

src/toasts/ToastsRegion.jsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import { Toast } from './Toast';
4-
import { toastsDefaultStylesByPosition } from './toasts-helpers';
4+
import { TOASTS_DEFAULT_STYLES_BY_POSITION, TOASTS_DEFAULT_STYLE } from './toasts-helpers';
55

66
export function ToastsRegion({ name, className, toasts, noStyle }) {
77
return (
@@ -12,10 +12,8 @@ export function ToastsRegion({ name, className, toasts, noStyle }) {
1212
noStyle
1313
? null
1414
: {
15-
position: 'fixed',
16-
zIndex: 9999,
17-
maxWidth: '50%',
18-
...toastsDefaultStylesByPosition[name],
15+
...TOASTS_DEFAULT_STYLE,
16+
...TOASTS_DEFAULT_STYLES_BY_POSITION[name],
1917
}
2018
}
2119
>

src/toasts/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './toasts-helpers';
22
export * from './ToastsContainer';
3+
export * from './useToasts';

src/toasts/toasts-helpers.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import React from 'react';
22

33
export const ToastsContext = React.createContext(null);
44

5-
export const toastPositions = [
5+
export const TOASTS_VALID_POSITIONS = ['TOP_LEFT', 'TOP_RIGHT', 'BOTTOM_LEFT', 'BOTTOM_RIGHT'];
6+
7+
export const TOASTS_VALID_TYPES = ['info', 'success', 'danger', 'warning'];
8+
9+
export const TOASTS_CLASSNAME_BY_POSITION = [
610
{
711
name: 'TOP_LEFT',
812
className: 'top-left',
@@ -21,7 +25,13 @@ export const toastPositions = [
2125
},
2226
];
2327

24-
export const toastsDefaultStylesByPosition = {
28+
export const TOASTS_DEFAULT_STYLE = {
29+
position: 'fixed',
30+
zIndex: 9999,
31+
maxWidth: '50%',
32+
};
33+
34+
export const TOASTS_DEFAULT_STYLES_BY_POSITION = {
2535
TOP_RIGHT: {
2636
top: '25px',
2737
right: '25px',

src/toasts/useToast.js

-43
This file was deleted.

src/toasts/useToastState.js

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { useState, useCallback, useEffect, useRef } from 'react';
2+
import { useArrayValueMap } from '../utils/useValueMap';
3+
import { TOASTS_VALID_TYPES, TOASTS_VALID_POSITIONS } from './toasts-helpers';
4+
5+
export function useToastState({ unique }) {
6+
const [nextId, setNextId] = useState(0);
7+
const timeoutRefs = useRef({});
8+
9+
const { push, unset, get, reset } = useArrayValueMap(
10+
unique && {
11+
equalityComparator: (a) => (b) => a.message === b.message,
12+
}
13+
);
14+
15+
const show = useCallback(
16+
(message, { type = 'info', autoClose = 5000, position = 'TOP_RIGHT' } = {}) => {
17+
if (!TOASTS_VALID_TYPES.includes(type)) {
18+
throw new Error(`Invalid toast type ${type}. Must be ${TOASTS_VALID_TYPES}`);
19+
}
20+
21+
if (!TOASTS_VALID_POSITIONS.includes(position)) {
22+
throw new Error(`Invalid toast position ${position}. Must be ${TOASTS_VALID_POSITIONS}`);
23+
}
24+
25+
const toastId = nextId;
26+
27+
push(position, {
28+
id: toastId,
29+
message,
30+
type,
31+
position,
32+
closeControl: !autoClose,
33+
});
34+
35+
if (typeof autoClose === 'number' && !isNaN(autoClose)) {
36+
const timeoutId = setTimeout(() => {
37+
close(position, toastId);
38+
}, autoClose);
39+
40+
timeoutRefs.current[toastId] = { timeoutId, position };
41+
// setToastTimeout(toastId, timeout);
42+
}
43+
44+
setNextId((prevId) => prevId + 1);
45+
46+
return toastId;
47+
},
48+
[close, nextId, push]
49+
);
50+
51+
const close = useCallback(
52+
(position, toastId) => {
53+
const { timeoutId } = timeoutRefs.current[toastId];
54+
55+
if (timeoutId) {
56+
clearTimeout(timeoutId);
57+
}
58+
59+
delete timeoutRefs.current[toastId];
60+
61+
unset(position, (toast) => toast.id !== toastId);
62+
},
63+
[unset]
64+
);
65+
66+
const closeAll = useCallback(() => {
67+
for (const [toastId, { position }] of Object.entries(timeoutRefs.current)) {
68+
close(position, toastId);
69+
}
70+
71+
reset();
72+
}, [close, reset]);
73+
74+
useEffect(
75+
() => closeAll,
76+
// eslint-disable-next-line react-hooks/exhaustive-deps
77+
[]
78+
);
79+
80+
return {
81+
show,
82+
close,
83+
closeAll,
84+
get,
85+
};
86+
}

src/toasts/useToasts.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useContext } from 'react';
2+
import { ToastsContext } from './toasts-helpers';
3+
4+
export function useToasts() {
5+
const toastsState = useContext(ToastsContext);
6+
7+
return {
8+
showToast(message, { type = 'info', autoClose = 5000, position = 'TOP_RIGHT' } = {}) {
9+
const toastId = toastsState.show(message, { type, autoClose, position });
10+
11+
return function closeToast() {
12+
toastsState.close(position, toastId);
13+
};
14+
},
15+
closeAllToasts() {
16+
toastsState.closeAll();
17+
},
18+
};
19+
}

src/utils/useValueMap.js

+13-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { useState } from 'react';
1+
import { useState, useCallback } from 'react';
22

33
export function useValueMap() {
44
const [valueMap, updateValueMap] = useState({});
55

6-
function setValue(key, _value) {
6+
const setValue = useCallback((key, _value) => {
77
updateValueMap((prevValueMap) => {
88
let value = _value;
99

@@ -16,11 +16,9 @@ export function useValueMap() {
1616
[key]: value,
1717
};
1818
});
19-
}
19+
}, []);
2020

21-
function getValue(key) {
22-
return valueMap[key];
23-
}
21+
const getValue = useCallback((key) => valueMap[key], [valueMap]);
2422

2523
return {
2624
setValue,
@@ -33,11 +31,17 @@ export function useValueMap() {
3331
setValue(key, value);
3432
}
3533
},
34+
unsetKey(key) {
35+
updateValueMap(({ [key]: _, ...prevValueMap }) => prevValueMap);
36+
},
37+
reset() {
38+
updateValueMap({});
39+
},
3640
};
3741
}
3842

3943
export function useArrayValueMap({ unique = true, equalityComparator = (a) => (b) => a === b } = {}) {
40-
const { getAllKeys, getValue, setValue } = useValueMap();
44+
const { getAllKeys, getValue, setValue, reset } = useValueMap();
4145

4246
return {
4347
push(key, value) {
@@ -56,9 +60,10 @@ export function useArrayValueMap({ unique = true, equalityComparator = (a) => (b
5660
});
5761
},
5862
unset(key, filterfn) {
59-
setValue(key, (prevValues) => prevValues.filter(filterfn));
63+
setValue(key, (prevValues) => prevValues && prevValues.filter(filterfn));
6064
},
6165
get: getValue,
6266
getAllKeys,
67+
reset,
6368
};
6469
}

0 commit comments

Comments
 (0)