UNPKG

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
"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;