@octanejs/hot-toast 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/LICENSE.upstream +21 -0
- package/README.md +76 -2
- package/UPSTREAM.md +91 -0
- package/package.json +67 -3
- package/src/components/checkmark.ts +18 -0
- package/src/components/error.ts +20 -0
- package/src/components/loader.ts +16 -0
- package/src/components/styled.tsrx +40 -0
- package/src/components/toast-bar.tsrx +81 -0
- package/src/components/toast-icon.tsrx +54 -0
- package/src/components/toaster.tsrx +135 -0
- package/src/core/store.ts +225 -0
- package/src/core/toast.ts +152 -0
- package/src/core/types.ts +94 -0
- package/src/core/use-toaster.ts +141 -0
- package/src/core/utils.ts +21 -0
- package/src/goober.d.ts +18 -0
- package/src/goober.js +6 -0
- package/src/headless/index.ts +27 -0
- package/src/index.ts +15 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Ported from react-hot-toast@2.6.1 src/core/store.ts
|
|
2
|
+
// (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
|
|
3
|
+
//
|
|
4
|
+
// Octane adaptations: the subscription hooks come from octane. useState's third
|
|
5
|
+
// tuple member is unused, and the setter keeps its identity across renders as
|
|
6
|
+
// React's does, so the listener registry finds it on unsubscribe.
|
|
7
|
+
import { useEffect, useRef, useState } from 'octane';
|
|
8
|
+
import type { DefaultToastOptions, Toast, ToastType } from './types';
|
|
9
|
+
|
|
10
|
+
export const TOAST_EXPIRE_DISMISS_DELAY = 1000;
|
|
11
|
+
export const TOAST_LIMIT = 20;
|
|
12
|
+
export const DEFAULT_TOASTER_ID = 'default';
|
|
13
|
+
|
|
14
|
+
interface ToasterSettings {
|
|
15
|
+
toastLimit: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export enum ActionType {
|
|
19
|
+
ADD_TOAST,
|
|
20
|
+
UPDATE_TOAST,
|
|
21
|
+
UPSERT_TOAST,
|
|
22
|
+
DISMISS_TOAST,
|
|
23
|
+
REMOVE_TOAST,
|
|
24
|
+
START_PAUSE,
|
|
25
|
+
END_PAUSE,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type Action =
|
|
29
|
+
| {
|
|
30
|
+
type: ActionType.ADD_TOAST;
|
|
31
|
+
toast: Toast;
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
type: ActionType.UPSERT_TOAST;
|
|
35
|
+
toast: Toast;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
type: ActionType.UPDATE_TOAST;
|
|
39
|
+
toast: Partial<Toast>;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
type: ActionType.DISMISS_TOAST;
|
|
43
|
+
toastId?: string;
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
type: ActionType.REMOVE_TOAST;
|
|
47
|
+
toastId?: string;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
type: ActionType.START_PAUSE;
|
|
51
|
+
time: number;
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
type: ActionType.END_PAUSE;
|
|
55
|
+
time: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
interface ToasterState {
|
|
59
|
+
toasts: Toast[];
|
|
60
|
+
settings: ToasterSettings;
|
|
61
|
+
pausedAt: number | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface State {
|
|
65
|
+
[toasterId: string]: ToasterState;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const reducer = (state: ToasterState, action: Action): ToasterState => {
|
|
69
|
+
const { toastLimit } = state.settings;
|
|
70
|
+
|
|
71
|
+
switch (action.type) {
|
|
72
|
+
case ActionType.ADD_TOAST:
|
|
73
|
+
return {
|
|
74
|
+
...state,
|
|
75
|
+
toasts: [action.toast, ...state.toasts].slice(0, toastLimit),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
case ActionType.UPDATE_TOAST:
|
|
79
|
+
return {
|
|
80
|
+
...state,
|
|
81
|
+
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
case ActionType.UPSERT_TOAST:
|
|
85
|
+
const { toast } = action;
|
|
86
|
+
return reducer(state, {
|
|
87
|
+
type: state.toasts.find((t) => t.id === toast.id)
|
|
88
|
+
? ActionType.UPDATE_TOAST
|
|
89
|
+
: ActionType.ADD_TOAST,
|
|
90
|
+
toast,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
case ActionType.DISMISS_TOAST:
|
|
94
|
+
const { toastId } = action;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
...state,
|
|
98
|
+
toasts: state.toasts.map((t) =>
|
|
99
|
+
t.id === toastId || toastId === undefined
|
|
100
|
+
? {
|
|
101
|
+
...t,
|
|
102
|
+
dismissed: true,
|
|
103
|
+
visible: false,
|
|
104
|
+
}
|
|
105
|
+
: t,
|
|
106
|
+
),
|
|
107
|
+
};
|
|
108
|
+
case ActionType.REMOVE_TOAST:
|
|
109
|
+
if (action.toastId === undefined) {
|
|
110
|
+
return {
|
|
111
|
+
...state,
|
|
112
|
+
toasts: [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
...state,
|
|
117
|
+
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
case ActionType.START_PAUSE:
|
|
121
|
+
return {
|
|
122
|
+
...state,
|
|
123
|
+
pausedAt: action.time,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
case ActionType.END_PAUSE:
|
|
127
|
+
const diff = action.time - (state.pausedAt || 0);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
...state,
|
|
131
|
+
pausedAt: undefined,
|
|
132
|
+
toasts: state.toasts.map((t) => ({
|
|
133
|
+
...t,
|
|
134
|
+
pauseDuration: t.pauseDuration + diff,
|
|
135
|
+
})),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const listeners: Array<[toasterId: string, reducer: (state: ToasterState) => void]> = [];
|
|
141
|
+
|
|
142
|
+
const defaultToasterState: ToasterState = {
|
|
143
|
+
toasts: [],
|
|
144
|
+
pausedAt: undefined,
|
|
145
|
+
settings: {
|
|
146
|
+
toastLimit: TOAST_LIMIT,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
let memoryState: State = {};
|
|
150
|
+
|
|
151
|
+
export const dispatch = (action: Action, toasterId = DEFAULT_TOASTER_ID) => {
|
|
152
|
+
memoryState[toasterId] = reducer(memoryState[toasterId] || defaultToasterState, action);
|
|
153
|
+
listeners.forEach(([id, listener]) => {
|
|
154
|
+
if (id === toasterId) {
|
|
155
|
+
listener(memoryState[toasterId]);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const dispatchAll = (action: Action) =>
|
|
161
|
+
Object.keys(memoryState).forEach((toasterId) => dispatch(action, toasterId));
|
|
162
|
+
|
|
163
|
+
export const getToasterIdFromToastId = (toastId: string) =>
|
|
164
|
+
Object.keys(memoryState).find((toasterId) =>
|
|
165
|
+
memoryState[toasterId].toasts.some((t) => t.id === toastId),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
export const createDispatch =
|
|
169
|
+
(toasterId = DEFAULT_TOASTER_ID) =>
|
|
170
|
+
(action: Action) => {
|
|
171
|
+
dispatch(action, toasterId);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const defaultTimeouts: {
|
|
175
|
+
[key in ToastType]: number;
|
|
176
|
+
} = {
|
|
177
|
+
blank: 4000,
|
|
178
|
+
error: 4000,
|
|
179
|
+
success: 2000,
|
|
180
|
+
loading: Infinity,
|
|
181
|
+
custom: 4000,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const useStore = (
|
|
185
|
+
toastOptions: DefaultToastOptions = {},
|
|
186
|
+
toasterId: string = DEFAULT_TOASTER_ID,
|
|
187
|
+
): ToasterState => {
|
|
188
|
+
const [state, setState] = useState<ToasterState>(memoryState[toasterId] || defaultToasterState);
|
|
189
|
+
const initial = useRef(memoryState[toasterId]);
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (initial.current !== memoryState[toasterId]) {
|
|
193
|
+
setState(memoryState[toasterId]);
|
|
194
|
+
}
|
|
195
|
+
listeners.push([toasterId, setState]);
|
|
196
|
+
return () => {
|
|
197
|
+
const index = listeners.findIndex(([id, setter]) => id === toasterId && setter === setState);
|
|
198
|
+
if (index > -1) {
|
|
199
|
+
listeners.splice(index, 1);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}, [toasterId]);
|
|
203
|
+
|
|
204
|
+
const mergedToasts = state.toasts.map((t) => ({
|
|
205
|
+
...toastOptions,
|
|
206
|
+
...toastOptions[t.type],
|
|
207
|
+
...t,
|
|
208
|
+
removeDelay: t.removeDelay || toastOptions[t.type]?.removeDelay || toastOptions?.removeDelay,
|
|
209
|
+
duration:
|
|
210
|
+
t.duration ||
|
|
211
|
+
toastOptions[t.type]?.duration ||
|
|
212
|
+
toastOptions?.duration ||
|
|
213
|
+
defaultTimeouts[t.type],
|
|
214
|
+
style: {
|
|
215
|
+
...toastOptions.style,
|
|
216
|
+
...toastOptions[t.type]?.style,
|
|
217
|
+
...t.style,
|
|
218
|
+
},
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
...state,
|
|
223
|
+
toasts: mergedToasts,
|
|
224
|
+
};
|
|
225
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Ported unchanged from react-hot-toast@2.6.1 src/core/toast.ts
|
|
2
|
+
// (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
|
|
3
|
+
import {
|
|
4
|
+
type DefaultToastOptions,
|
|
5
|
+
type Renderable,
|
|
6
|
+
type Toast,
|
|
7
|
+
type ToastOptions,
|
|
8
|
+
type ToastType,
|
|
9
|
+
type ValueOrFunction,
|
|
10
|
+
resolveValue,
|
|
11
|
+
} from './types';
|
|
12
|
+
import { genId } from './utils';
|
|
13
|
+
import {
|
|
14
|
+
type Action,
|
|
15
|
+
ActionType,
|
|
16
|
+
createDispatch,
|
|
17
|
+
dispatchAll,
|
|
18
|
+
getToasterIdFromToastId,
|
|
19
|
+
} from './store';
|
|
20
|
+
|
|
21
|
+
type Message = ValueOrFunction<Renderable, Toast>;
|
|
22
|
+
|
|
23
|
+
type ToastHandler = (message: Message, options?: ToastOptions) => string;
|
|
24
|
+
|
|
25
|
+
const createToast = (message: Message, type: ToastType = 'blank', opts?: ToastOptions): Toast => ({
|
|
26
|
+
createdAt: Date.now(),
|
|
27
|
+
visible: true,
|
|
28
|
+
dismissed: false,
|
|
29
|
+
type,
|
|
30
|
+
ariaProps: {
|
|
31
|
+
role: 'status',
|
|
32
|
+
'aria-live': 'polite',
|
|
33
|
+
},
|
|
34
|
+
message,
|
|
35
|
+
pauseDuration: 0,
|
|
36
|
+
...opts,
|
|
37
|
+
id: opts?.id || genId(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const createHandler =
|
|
41
|
+
(type?: ToastType): ToastHandler =>
|
|
42
|
+
(message, options) => {
|
|
43
|
+
const toast = createToast(message, type, options);
|
|
44
|
+
|
|
45
|
+
const dispatch = createDispatch(toast.toasterId || getToasterIdFromToastId(toast.id));
|
|
46
|
+
|
|
47
|
+
dispatch({ type: ActionType.UPSERT_TOAST, toast });
|
|
48
|
+
return toast.id;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const toast = (message: Message, opts?: ToastOptions) => createHandler('blank')(message, opts);
|
|
52
|
+
|
|
53
|
+
toast.error = createHandler('error');
|
|
54
|
+
toast.success = createHandler('success');
|
|
55
|
+
toast.loading = createHandler('loading');
|
|
56
|
+
toast.custom = createHandler('custom');
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Dismisses the toast with the given id. If no id is given, dismisses all toasts.
|
|
60
|
+
* The toast will transition out and then be removed from the DOM.
|
|
61
|
+
* Applies to all toasters, except when a `toasterId` is given.
|
|
62
|
+
*/
|
|
63
|
+
toast.dismiss = (toastId?: string, toasterId?: string) => {
|
|
64
|
+
const action: Action = {
|
|
65
|
+
type: ActionType.DISMISS_TOAST,
|
|
66
|
+
toastId,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (toasterId) {
|
|
70
|
+
createDispatch(toasterId)(action);
|
|
71
|
+
} else {
|
|
72
|
+
dispatchAll(action);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Dismisses all toasts.
|
|
78
|
+
*/
|
|
79
|
+
toast.dismissAll = (toasterId?: string) => toast.dismiss(undefined, toasterId);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Removes the toast with the given id.
|
|
83
|
+
* The toast will be removed from the DOM without any transition.
|
|
84
|
+
*/
|
|
85
|
+
toast.remove = (toastId?: string, toasterId?: string) => {
|
|
86
|
+
const action: Action = {
|
|
87
|
+
type: ActionType.REMOVE_TOAST,
|
|
88
|
+
toastId,
|
|
89
|
+
};
|
|
90
|
+
if (toasterId) {
|
|
91
|
+
createDispatch(toasterId)(action);
|
|
92
|
+
} else {
|
|
93
|
+
dispatchAll(action);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Removes all toasts.
|
|
99
|
+
*/
|
|
100
|
+
toast.removeAll = (toasterId?: string) => toast.remove(undefined, toasterId);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create a loading toast that will automatically updates with the promise.
|
|
104
|
+
*/
|
|
105
|
+
toast.promise = <T>(
|
|
106
|
+
promise: Promise<T> | (() => Promise<T>),
|
|
107
|
+
msgs: {
|
|
108
|
+
loading: Renderable;
|
|
109
|
+
success?: ValueOrFunction<Renderable, T>;
|
|
110
|
+
error?: ValueOrFunction<Renderable, any>;
|
|
111
|
+
},
|
|
112
|
+
opts?: DefaultToastOptions,
|
|
113
|
+
) => {
|
|
114
|
+
const id = toast.loading(msgs.loading, { ...opts, ...opts?.loading });
|
|
115
|
+
|
|
116
|
+
if (typeof promise === 'function') {
|
|
117
|
+
promise = promise();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
promise
|
|
121
|
+
.then((p) => {
|
|
122
|
+
const successMessage = msgs.success ? resolveValue(msgs.success, p) : undefined;
|
|
123
|
+
|
|
124
|
+
if (successMessage) {
|
|
125
|
+
toast.success(successMessage, {
|
|
126
|
+
id,
|
|
127
|
+
...opts,
|
|
128
|
+
...opts?.success,
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
toast.dismiss(id);
|
|
132
|
+
}
|
|
133
|
+
return p;
|
|
134
|
+
})
|
|
135
|
+
.catch((e) => {
|
|
136
|
+
const errorMessage = msgs.error ? resolveValue(msgs.error, e) : undefined;
|
|
137
|
+
|
|
138
|
+
if (errorMessage) {
|
|
139
|
+
toast.error(errorMessage, {
|
|
140
|
+
id,
|
|
141
|
+
...opts,
|
|
142
|
+
...opts?.error,
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
toast.dismiss(id);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return promise;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export { toast };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Ported from react-hot-toast@2.6.1 src/core/types.ts
|
|
2
|
+
// (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
|
|
3
|
+
//
|
|
4
|
+
// Octane adaptations: React element and style types become Octane's
|
|
5
|
+
// ElementDescriptor, OctaneNode and CSSProperties.
|
|
6
|
+
import type { CSSProperties, ElementDescriptor, OctaneNode } from 'octane';
|
|
7
|
+
|
|
8
|
+
export type ToastType = 'success' | 'error' | 'loading' | 'blank' | 'custom';
|
|
9
|
+
export type ToastPosition =
|
|
10
|
+
'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
11
|
+
|
|
12
|
+
export type Renderable = ElementDescriptor<any> | string | null;
|
|
13
|
+
|
|
14
|
+
export interface IconTheme {
|
|
15
|
+
primary: string;
|
|
16
|
+
secondary: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ValueFunction<TValue, TArg> = (arg: TArg) => TValue;
|
|
20
|
+
export type ValueOrFunction<TValue, TArg> = TValue | ValueFunction<TValue, TArg>;
|
|
21
|
+
|
|
22
|
+
const isFunction = <TValue, TArg>(
|
|
23
|
+
valOrFunction: ValueOrFunction<TValue, TArg>,
|
|
24
|
+
): valOrFunction is ValueFunction<TValue, TArg> => typeof valOrFunction === 'function';
|
|
25
|
+
|
|
26
|
+
export const resolveValue = <TValue, TArg>(
|
|
27
|
+
valOrFunction: ValueOrFunction<TValue, TArg>,
|
|
28
|
+
arg: TArg,
|
|
29
|
+
): TValue => (isFunction(valOrFunction) ? valOrFunction(arg) : valOrFunction);
|
|
30
|
+
|
|
31
|
+
export interface Toast {
|
|
32
|
+
type: ToastType;
|
|
33
|
+
id: string;
|
|
34
|
+
toasterId?: string;
|
|
35
|
+
message: ValueOrFunction<Renderable, Toast>;
|
|
36
|
+
icon?: Renderable;
|
|
37
|
+
duration?: number;
|
|
38
|
+
pauseDuration: number;
|
|
39
|
+
position?: ToastPosition;
|
|
40
|
+
removeDelay?: number;
|
|
41
|
+
|
|
42
|
+
ariaProps: {
|
|
43
|
+
role: 'status' | 'alert';
|
|
44
|
+
'aria-live': 'assertive' | 'off' | 'polite';
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
style?: CSSProperties;
|
|
48
|
+
className?: string;
|
|
49
|
+
iconTheme?: IconTheme;
|
|
50
|
+
|
|
51
|
+
createdAt: number;
|
|
52
|
+
visible: boolean;
|
|
53
|
+
dismissed: boolean;
|
|
54
|
+
height?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type ToastOptions = Partial<
|
|
58
|
+
Pick<
|
|
59
|
+
Toast,
|
|
60
|
+
| 'id'
|
|
61
|
+
| 'icon'
|
|
62
|
+
| 'duration'
|
|
63
|
+
| 'ariaProps'
|
|
64
|
+
| 'className'
|
|
65
|
+
| 'style'
|
|
66
|
+
| 'position'
|
|
67
|
+
| 'iconTheme'
|
|
68
|
+
| 'toasterId'
|
|
69
|
+
| 'removeDelay'
|
|
70
|
+
>
|
|
71
|
+
>;
|
|
72
|
+
|
|
73
|
+
export type DefaultToastOptions = ToastOptions & {
|
|
74
|
+
[key in ToastType]?: ToastOptions;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export interface ToasterProps {
|
|
78
|
+
position?: ToastPosition;
|
|
79
|
+
toastOptions?: DefaultToastOptions;
|
|
80
|
+
reverseOrder?: boolean;
|
|
81
|
+
gutter?: number;
|
|
82
|
+
containerStyle?: CSSProperties;
|
|
83
|
+
containerClassName?: string;
|
|
84
|
+
toasterId?: string;
|
|
85
|
+
children?: (toast: Toast) => ElementDescriptor<any>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ToastWrapperProps {
|
|
89
|
+
id: string;
|
|
90
|
+
className?: string;
|
|
91
|
+
style?: CSSProperties;
|
|
92
|
+
onHeightUpdate: (id: string, height: number) => void;
|
|
93
|
+
children?: OctaneNode;
|
|
94
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Ported from react-hot-toast@2.6.1 src/core/use-toaster.ts
|
|
2
|
+
// (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
|
|
3
|
+
//
|
|
4
|
+
// Octane adaptations: the hooks come from octane. Every dependency array is
|
|
5
|
+
// upstream's own, so Octane keeps React's exact behavior for each of them.
|
|
6
|
+
// Upstream also imports the store's `dispatch`, but the local `dispatch` below
|
|
7
|
+
// shadows it everywhere in this hook, so the unused import is dropped.
|
|
8
|
+
import { useCallback, useEffect, useRef } from 'octane';
|
|
9
|
+
import { ActionType, createDispatch, useStore } from './store';
|
|
10
|
+
import { toast } from './toast';
|
|
11
|
+
import type { DefaultToastOptions, Toast, ToastPosition } from './types';
|
|
12
|
+
|
|
13
|
+
export const REMOVE_DELAY = 1000;
|
|
14
|
+
|
|
15
|
+
export const useToaster = (toastOptions?: DefaultToastOptions, toasterId: string = 'default') => {
|
|
16
|
+
const { toasts, pausedAt } = useStore(toastOptions, toasterId);
|
|
17
|
+
const toastTimeouts = useRef(new Map<Toast['id'], ReturnType<typeof setTimeout>>()).current;
|
|
18
|
+
|
|
19
|
+
const addToRemoveQueue = useCallback((toastId: string, removeDelay = REMOVE_DELAY) => {
|
|
20
|
+
if (toastTimeouts.has(toastId)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const timeout = setTimeout(() => {
|
|
25
|
+
toastTimeouts.delete(toastId);
|
|
26
|
+
dispatch({
|
|
27
|
+
type: ActionType.REMOVE_TOAST,
|
|
28
|
+
toastId: toastId,
|
|
29
|
+
});
|
|
30
|
+
}, removeDelay);
|
|
31
|
+
|
|
32
|
+
toastTimeouts.set(toastId, timeout);
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (pausedAt) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const timeouts = toasts.map((t) => {
|
|
42
|
+
if (t.duration === Infinity) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const durationLeft = (t.duration || 0) + t.pauseDuration - (now - t.createdAt);
|
|
47
|
+
|
|
48
|
+
if (durationLeft < 0) {
|
|
49
|
+
if (t.visible) {
|
|
50
|
+
toast.dismiss(t.id);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
return setTimeout(() => toast.dismiss(t.id, toasterId), durationLeft);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
timeouts.forEach((timeout) => timeout && clearTimeout(timeout));
|
|
59
|
+
};
|
|
60
|
+
}, [toasts, pausedAt, toasterId]);
|
|
61
|
+
|
|
62
|
+
const dispatch = useCallback(createDispatch(toasterId), [toasterId]);
|
|
63
|
+
|
|
64
|
+
const startPause = useCallback(() => {
|
|
65
|
+
dispatch({
|
|
66
|
+
type: ActionType.START_PAUSE,
|
|
67
|
+
time: Date.now(),
|
|
68
|
+
});
|
|
69
|
+
}, [dispatch]);
|
|
70
|
+
|
|
71
|
+
const updateHeight = useCallback(
|
|
72
|
+
(toastId: string, height: number) => {
|
|
73
|
+
dispatch({
|
|
74
|
+
type: ActionType.UPDATE_TOAST,
|
|
75
|
+
toast: { id: toastId, height },
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
[dispatch],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const endPause = useCallback(() => {
|
|
82
|
+
if (pausedAt) {
|
|
83
|
+
dispatch({ type: ActionType.END_PAUSE, time: Date.now() });
|
|
84
|
+
}
|
|
85
|
+
}, [pausedAt, dispatch]);
|
|
86
|
+
|
|
87
|
+
const calculateOffset = useCallback(
|
|
88
|
+
(
|
|
89
|
+
toast: Toast,
|
|
90
|
+
opts?: {
|
|
91
|
+
reverseOrder?: boolean;
|
|
92
|
+
gutter?: number;
|
|
93
|
+
defaultPosition?: ToastPosition;
|
|
94
|
+
},
|
|
95
|
+
) => {
|
|
96
|
+
const { reverseOrder = false, gutter = 8, defaultPosition } = opts || {};
|
|
97
|
+
|
|
98
|
+
const relevantToasts = toasts.filter(
|
|
99
|
+
(t) => (t.position || defaultPosition) === (toast.position || defaultPosition) && t.height,
|
|
100
|
+
);
|
|
101
|
+
const toastIndex = relevantToasts.findIndex((t) => t.id === toast.id);
|
|
102
|
+
const toastsBefore = relevantToasts.filter(
|
|
103
|
+
(toast, i) => i < toastIndex && toast.visible,
|
|
104
|
+
).length;
|
|
105
|
+
|
|
106
|
+
const offset = relevantToasts
|
|
107
|
+
.filter((t) => t.visible)
|
|
108
|
+
.slice(...(reverseOrder ? [toastsBefore + 1] : [0, toastsBefore]))
|
|
109
|
+
.reduce((acc, t) => acc + (t.height || 0) + gutter, 0);
|
|
110
|
+
|
|
111
|
+
return offset;
|
|
112
|
+
},
|
|
113
|
+
[toasts],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Keep track of dismissed toasts and remove them after the delay
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
toasts.forEach((toast) => {
|
|
119
|
+
if (toast.dismissed) {
|
|
120
|
+
addToRemoveQueue(toast.id, toast.removeDelay);
|
|
121
|
+
} else {
|
|
122
|
+
// If toast becomes visible again, remove it from the queue
|
|
123
|
+
const timeout = toastTimeouts.get(toast.id);
|
|
124
|
+
if (timeout) {
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
toastTimeouts.delete(toast.id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}, [toasts, addToRemoveQueue]);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
toasts,
|
|
134
|
+
handlers: {
|
|
135
|
+
updateHeight,
|
|
136
|
+
startPause,
|
|
137
|
+
endPause,
|
|
138
|
+
calculateOffset,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Ported unchanged from react-hot-toast@2.6.1 src/core/utils.ts
|
|
2
|
+
// (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
|
|
3
|
+
export const genId = (() => {
|
|
4
|
+
let count = 0;
|
|
5
|
+
return () => {
|
|
6
|
+
return (++count).toString();
|
|
7
|
+
};
|
|
8
|
+
})();
|
|
9
|
+
|
|
10
|
+
export const prefersReducedMotion = (() => {
|
|
11
|
+
// Cache result
|
|
12
|
+
let shouldReduceMotion: boolean | undefined = undefined;
|
|
13
|
+
|
|
14
|
+
return () => {
|
|
15
|
+
if (shouldReduceMotion === undefined && typeof window !== 'undefined') {
|
|
16
|
+
const mediaQuery = matchMedia('(prefers-reduced-motion: reduce)');
|
|
17
|
+
shouldReduceMotion = !mediaQuery || mediaQuery.matches;
|
|
18
|
+
}
|
|
19
|
+
return shouldReduceMotion;
|
|
20
|
+
};
|
|
21
|
+
})();
|
package/src/goober.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** goober's styled() context: the element props and the prepend flag. */
|
|
2
|
+
export interface GooberContext {
|
|
3
|
+
p?: object;
|
|
4
|
+
o?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Compiles and injects a rule, returning its generated class name. */
|
|
8
|
+
export declare function css(
|
|
9
|
+
this: GooberContext | void,
|
|
10
|
+
tag: TemplateStringsArray | string,
|
|
11
|
+
...props: Array<string | number | ((props: any) => string | number)>
|
|
12
|
+
): string;
|
|
13
|
+
|
|
14
|
+
/** Compiles and injects an @keyframes rule, returning its generated name. */
|
|
15
|
+
export declare function keyframes(
|
|
16
|
+
tag: TemplateStringsArray | string,
|
|
17
|
+
...props: Array<string | number>
|
|
18
|
+
): string;
|
package/src/goober.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// goober's runtime, unchanged. goober 2.1.19 declares its types with `export =`
|
|
2
|
+
// inside a "type": "module" package, which TypeScript rejects (TS1203) whenever
|
|
3
|
+
// library checking is on (cristianbote/goober#606). Importing goober through this
|
|
4
|
+
// module keeps that declaration file out of every consumer's program; the
|
|
5
|
+
// sibling goober.d.ts types the part react-hot-toast uses.
|
|
6
|
+
export { css, keyframes } from 'goober';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Ported unchanged from react-hot-toast@2.6.1 src/headless/index.ts
|
|
2
|
+
// (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
|
|
3
|
+
//
|
|
4
|
+
// OCTANE DIVERGENCE[shared-headless-store][conformance:hot-toast-shared-headless-store]:
|
|
5
|
+
// upstream bundles this entry separately, with its own copy of the store. The
|
|
6
|
+
// port publishes its source, so this entry and the root share one store.
|
|
7
|
+
import { toast } from '../core/toast';
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
DefaultToastOptions,
|
|
11
|
+
IconTheme,
|
|
12
|
+
Renderable,
|
|
13
|
+
Toast,
|
|
14
|
+
ToasterProps,
|
|
15
|
+
ToastOptions,
|
|
16
|
+
ToastPosition,
|
|
17
|
+
ToastType,
|
|
18
|
+
ValueFunction,
|
|
19
|
+
ValueOrFunction,
|
|
20
|
+
} from '../core/types';
|
|
21
|
+
|
|
22
|
+
export { resolveValue } from '../core/types';
|
|
23
|
+
export { useToaster } from '../core/use-toaster';
|
|
24
|
+
export { useStore as useToasterStore } from '../core/store';
|
|
25
|
+
|
|
26
|
+
export { toast };
|
|
27
|
+
export default toast;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Ported from react-hot-toast@2.6.1 src/index.ts
|
|
2
|
+
// (https://github.com/timolins/react-hot-toast/tree/v2.6.1).
|
|
3
|
+
import { toast } from './core/toast';
|
|
4
|
+
|
|
5
|
+
export * from './headless';
|
|
6
|
+
|
|
7
|
+
export { ToastBar } from './components/toast-bar.tsrx';
|
|
8
|
+
export { ToastIcon } from './components/toast-icon.tsrx';
|
|
9
|
+
export { Toaster } from './components/toaster.tsrx';
|
|
10
|
+
export { CheckmarkIcon } from './components/checkmark';
|
|
11
|
+
export { ErrorIcon } from './components/error';
|
|
12
|
+
export { LoaderIcon } from './components/loader';
|
|
13
|
+
|
|
14
|
+
export { toast };
|
|
15
|
+
export default toast;
|