notistack-v2-maintained
Version:
Highly customizable notification snackbars (toasts) that can be stacked on top of each other
230 lines (229 loc) • 12.5 kB
JavaScript
"use strict";
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SnackbarProvider = exports.SnackbarContext = void 0;
exports.useSnackbar = useSnackbar;
const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
const react_1 = require("@emotion/react");
const clsx_1 = __importDefault(require("clsx"));
const react_2 = require("react");
const react_dom_1 = require("react-dom");
const SnackbarContainer_1 = __importDefault(require("./SnackbarContainer"));
const SnackbarItem_1 = __importDefault(require("./SnackbarItem"));
const constants_1 = require("./constants");
exports.SnackbarContext = (0, react_2.createContext)({
enqueueSnackbar: () => null,
closeSnackbar: () => null
});
function useSnackbar() {
return (0, react_2.useContext)(exports.SnackbarContext);
}
class SnackbarProvider extends react_2.Component {
constructor(props) {
super(props);
/**
* Adds a new snackbar to the queue to be presented.
* Returns generated or user defined key referencing the new snackbar or null
*/
this.enqueueSnackbar = (message, opts = {}) => {
const { key, preventDuplicate } = opts, options = __rest(opts, ["key", "preventDuplicate"]);
const hasSpecifiedKey = (0, constants_1.isDefined)(key);
const id = hasSpecifiedKey
? key
: new Date().getTime() + Math.random();
const numberOrNull = (numberish) => typeof numberish === "number" || numberish === null;
const merger = (name) => {
if (name === "autoHideDuration") {
if (numberOrNull(options.autoHideDuration))
return options.autoHideDuration;
if (numberOrNull(this.props.autoHideDuration))
return this.props.autoHideDuration;
return constants_1.DEFAULTS.autoHideDuration;
}
return options[name] || this.props[name] || constants_1.DEFAULTS[name];
};
const snack = Object.assign(Object.assign({ key: id }, options), { message, open: true, entered: false, requestClose: false, variant: merger("variant"), anchorOrigin: merger("anchorOrigin"), autoHideDuration: merger("autoHideDuration") });
if (options.persist) {
snack.autoHideDuration = undefined;
}
this.setState((state) => {
if ((preventDuplicate === undefined && this.props.preventDuplicate) ||
preventDuplicate) {
const compareFunction = (item) => hasSpecifiedKey ? item.key === key : item.message === message;
const inQueue = state.queue.findIndex(compareFunction) > -1;
const inView = state.snacks.findIndex(compareFunction) > -1;
if (inQueue || inView) {
return state;
}
}
return this.handleDisplaySnack(Object.assign(Object.assign({}, state), { queue: [...state.queue, snack] }));
});
return id;
};
/**
* Reducer: Display snack if there's space for it. Otherwise, immediately
* begin dismissing the oldest message to start showing the new one.
*/
this.handleDisplaySnack = (state) => {
const { snacks } = state;
if (snacks.length >= this.maxSnack) {
return this.handleDismissOldest(state);
}
return this.processQueue(state);
};
/**
* Reducer: Display items (notifications) in the queue if there's space for them.
*/
this.processQueue = (state) => {
const { queue, snacks } = state;
if (queue.length > 0) {
return Object.assign(Object.assign({}, state), { snacks: [...snacks, queue[0]], queue: queue.slice(1, queue.length) });
}
return state;
};
/**
* Reducer: Hide oldest snackbar on the screen because there exists a new one which we have to display.
* (ignoring the one with 'persist' flag. i.e. explicitly told by user not to get dismissed).
*
* Note 1: If there is already a message leaving the screen, no new messages are dismissed.
* Note 2: If the oldest message has not yet entered the screen, only a request to close the
* snackbar is made. Once it entered the screen, it will be immediately dismissed.
*/
this.handleDismissOldest = (state) => {
if (state.snacks.some((item) => !item.open || item.requestClose)) {
return state;
}
let popped = false;
let ignore = false;
const persistentCount = state.snacks.reduce((acc, current) => acc + (current.open && current.persist ? 1 : 0), 0);
if (persistentCount === this.maxSnack) {
ignore = true;
}
const snacks = state.snacks.map((item) => {
var _a, _b, _c;
if (!popped && (!item.persist || ignore)) {
popped = true;
if (!item.entered) {
return Object.assign(Object.assign({}, item), { requestClose: true });
}
(_a = item.onClose) === null || _a === void 0 ? void 0 : _a.call(item, null, constants_1.REASONS.MAXSNACK, item.key);
(_c = (_b = this.props).onClose) === null || _c === void 0 ? void 0 : _c.call(_b, null, constants_1.REASONS.MAXSNACK, item.key);
return Object.assign(Object.assign({}, item), { open: false });
}
return Object.assign({}, item);
});
return Object.assign(Object.assign({}, state), { snacks });
};
/**
* Set the entered state of the snackbar with the given key.
*/
this.handleEnteredSnack = (node, isAppearing, key) => {
if (!(0, constants_1.isDefined)(key)) {
throw new Error("handleEnteredSnack Cannot be called with undefined key");
}
this.setState(({ snacks }) => ({
snacks: snacks.map((item) => item.key === key ? Object.assign(Object.assign({}, item), { entered: true }) : Object.assign({}, item))
}));
};
/**
* Hide a snackbar after its timeout.
*/
this.handleCloseSnack = (event, reason, key) => {
var _a, _b;
(_b = (_a = this.props).onClose) === null || _b === void 0 ? void 0 : _b.call(_a, event, reason, key);
if (reason === constants_1.REASONS.CLICKAWAY)
return;
const shouldCloseAll = key === undefined;
this.setState(({ snacks, queue }) => ({
snacks: snacks.map((item) => {
if (!shouldCloseAll && item.key !== key) {
return Object.assign({}, item);
}
return item.entered
? Object.assign(Object.assign({}, item), { open: false }) : Object.assign(Object.assign({}, item), { requestClose: true });
}),
queue: queue.filter((item) => item.key !== key)
}));
};
/**
* Close snackbar with the given key
*/
this.closeSnackbar = (key) => {
var _a;
// call individual snackbar onClose callback passed through options parameter
const toBeClosed = this.state.snacks.find((item) => item.key === key);
if ((0, constants_1.isDefined)(key)) {
(_a = toBeClosed === null || toBeClosed === void 0 ? void 0 : toBeClosed.onClose) === null || _a === void 0 ? void 0 : _a.call(toBeClosed, null, constants_1.REASONS.INSTRUCTED, key);
}
this.handleCloseSnack(null, constants_1.REASONS.INSTRUCTED, key);
};
/**
* When we set open attribute of a snackbar to false (i.e. after we hide a snackbar),
* it leaves the screen and immediately after leaving animation is done, this method
* gets called. We remove the hidden snackbar from state and then display notifications
* waiting in the queue (if any). If after this process the queue is not empty, the
* oldest message is dismissed.
*/
// @ts-expect-error idk why
this.handleExitedSnack = (event, key1, key2) => {
const key = key1 || key2;
if (!(0, constants_1.isDefined)(key)) {
throw new Error("handleExitedSnack Cannot be called with undefined key");
}
this.setState((state) => {
const newState = this.processQueue(Object.assign(Object.assign({}, state), { snacks: state.snacks.filter((item) => item.key !== key) }));
if (newState.queue.length === 0) {
return newState;
}
return this.handleDismissOldest(newState);
});
};
this.state = {
snacks: [],
queue: [],
contextValue: {
enqueueSnackbar: this.enqueueSnackbar.bind(this),
closeSnackbar: this.closeSnackbar.bind(this)
}
};
}
get maxSnack() {
return this.props.maxSnack || constants_1.DEFAULTS.maxSnack;
}
render() {
const { contextValue } = this.state;
const _a = this.props, { maxSnack: dontspread1, preventDuplicate: dontspread2, variant: dontspread3, anchorOrigin: dontspread4, iconVariant, dense = constants_1.DEFAULTS.dense, hideIconVariant = constants_1.DEFAULTS.hideIconVariant, domRoot, children, classes = {} } = _a, props = __rest(_a, ["maxSnack", "preventDuplicate", "variant", "anchorOrigin", "iconVariant", "dense", "hideIconVariant", "domRoot", "children", "classes"]);
const categ = this.state.snacks.reduce((acc, current) => {
const category = (0, constants_1.originKeyExtractor)(current.anchorOrigin);
const existingOfCategory = acc[category] || [];
return Object.assign(Object.assign({}, acc), { [category]: [...existingOfCategory, current] });
}, {});
const snackbars = Object.keys(categ).map((origin) => {
const snacks = categ[origin];
return ((0, jsx_runtime_1.jsx)(SnackbarContainer_1.default, { dense: dense, anchorOrigin: snacks[0].anchorOrigin, className: (0, clsx_1.default)(classes.containerRoot, classes[constants_1.transformer.toContainerAnchorOrigin(origin)]), children: snacks.map((snack) => ((0, react_1.createElement)(SnackbarItem_1.default, Object.assign({}, props, { key: snack.key, snack: snack, dense: dense, iconVariant: iconVariant, hideIconVariant: hideIconVariant, classes: (0, constants_1.omitContainerKeys)(classes), onClose: this.handleCloseSnack, onExited: (node, key) => {
var _a, _b;
this.handleExitedSnack(node, key);
(_b = (_a = this.props).onExited) === null || _b === void 0 ? void 0 : _b.call(_a, node, key);
}, onEntered: (node, isAppearing, key) => {
var _a, _b;
this.handleEnteredSnack(node, isAppearing, key);
(_b = (_a = this.props).onEntered) === null || _b === void 0 ? void 0 : _b.call(_a, node, isAppearing, key);
} })))) }, origin));
});
return ((0, jsx_runtime_1.jsxs)(exports.SnackbarContext.Provider, { value: contextValue, children: [children, domRoot ? (0, react_dom_1.createPortal)(snackbars, domRoot) : snackbars] }));
}
}
exports.SnackbarProvider = SnackbarProvider;