UNPKG

@blueprintjs/core

Version:

Core styles & components

247 lines 10.8 kB
"use strict"; /* * Copyright 2021 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.OverlayToaster = exports.OVERLAY_TOASTER_DELAY_MS = void 0; const tslib_1 = require("tslib"); const classnames_1 = tslib_1.__importDefault(require("classnames")); const React = tslib_1.__importStar(require("react")); const ReactDOMClient = tslib_1.__importStar(require("react-dom/client")); const common_1 = require("../../common"); const errors_1 = require("../../common/errors"); const props_1 = require("../../common/props"); const utils_1 = require("../../common/utils"); const overlay2_1 = require("../overlay2/overlay2"); const toast_1 = require("./toast"); const defaultDomRenderer = (element, container) => { ReactDOMClient.createRoot(container).render(element); }; exports.OVERLAY_TOASTER_DELAY_MS = 50; /** * OverlayToaster component. * * @see https://blueprintjs.com/docs/#core/components/toast */ class OverlayToaster extends common_1.AbstractPureComponent { constructor() { super(...arguments); this.state = { toastRefs: {}, toasts: [], }; // Queue of toasts to be displayed. If toasts are shown too quickly back to back, it can result in cut off toasts. // The queue ensures that toasts are only displayed in QUEUE_TIMEOUT_MS increments. this.queue = { cancel: undefined, isRunning: false, toasts: [], }; // auto-incrementing identifier for un-keyed toasts this.toastId = 0; this.toastRefs = {}; /** Compute a new collection of toast refs (usually after updating toasts) */ this.getToastRefs = (toasts) => { return toasts.reduce((refs, toast) => { refs[toast.key] = React.createRef(); return refs; }, {}); }; this.handleQueueTimeout = () => { const nextToast = this.queue.toasts.shift(); if (nextToast != null) { this.immediatelyShowToast(nextToast); this.startQueueTimeout(); } else { this.queue.isRunning = false; } }; this.renderToast = (toast) => { return React.createElement(toast_1.Toast, { ...toast, onDismiss: this.getDismissHandler(toast) }); }; this.getDismissHandler = (toast) => (timeoutExpired) => { this.dismiss(toast.key, timeoutExpired); }; this.handleClose = (e) => { // NOTE that `e` isn't always a KeyboardEvent but that's the only type we care about if (e.key === "Escape") { this.clear(); } }; } /** * Create a new `Toaster` instance that can be shared around your application. * The `Toaster` will be rendered into a new element appended to the given container. */ static create(props, options = {}) { if (props != null && props.usePortal != null && !(0, utils_1.isNodeEnv)("production")) { console.warn(errors_1.TOASTER_WARN_INLINE); } const { container = document.body, domRenderer = defaultDomRenderer } = options; const toasterComponentRoot = document.createElement("div"); container.appendChild(toasterComponentRoot); return new Promise((resolve, reject) => { try { domRenderer(React.createElement(OverlayToaster, { ...props, ref: handleRef, usePortal: false }), toasterComponentRoot); } catch (error) { // Note that we're catching errors from the domRenderer function // call, but not errors when rendering <OverlayToaster>, which // happens in a separate scheduled tick. Wrapping the // OverlayToaster in an error boundary would be necessary to // capture rendering errors, but that's still a bit unreliable // and would only catch errors rendering the initial mount. reject(error); } // We can get a rough guarantee that the OverlayToaster has been // mounted to the DOM by waiting until the ref callback here has // been fired. // // This is the approach suggested under "What about the render // callback?" at https://github.com/reactwg/react-18/discussions/5. function handleRef(ref) { if (ref == null) { reject(new Error(errors_1.TOASTER_CREATE_NULL)); return; } resolve(ref); } }); } /** * This is an alias for `OverlayToaster.create`, exposed for backwards compatibility with the 5.x API. * * @deprecated Use `OverlayToaster.create` instead. */ static createAsync(props, options) { return OverlayToaster.create(props, options); } show(props, key) { const options = this.createToastOptions(props, key); const wasExistingToastUpdated = this.maybeUpdateExistingToast(options, key); if (wasExistingToastUpdated) { return options.key; } if (this.queue.isRunning) { // If a toast has been shown recently, push to the queued toasts to prevent toasts from being shown too // quickly for the animations to keep up this.queue.toasts.push(options); } else { // If we have not recently shown a toast, we can immediately show the given toast this.immediatelyShowToast(options); this.startQueueTimeout(); } return options.key; } maybeUpdateExistingToast(options, key) { if (key == null) { return false; } const isExistingQueuedToast = this.queue.toasts.some(toast => toast.key === key); if (isExistingQueuedToast) { this.queue.toasts = this.queue.toasts.map(t => (t.key === key ? options : t)); return true; } const isExistingShownToast = this.state.toasts.some(toast => toast.key === key); if (isExistingShownToast) { this.updateToastsInState(toasts => toasts.map(t => (t.key === key ? options : t))); return true; } return false; } immediatelyShowToast(options) { if (this.props.maxToasts) { // check if active number of toasts are at the maxToasts limit this.dismissIfAtLimit(); } this.updateToastsInState(toasts => [options, ...toasts]); } startQueueTimeout() { this.queue.isRunning = true; this.queue.cancel = this.setTimeout(this.handleQueueTimeout, exports.OVERLAY_TOASTER_DELAY_MS); } updateToastsInState(getNewToasts) { this.setState(prevState => { const toasts = getNewToasts(prevState.toasts); return { toastRefs: this.getToastRefs(toasts), toasts }; }); } dismiss(key, timeoutExpired = false) { this.setState(prevState => { const toasts = prevState.toasts.filter(t => { var _a; const matchesKey = t.key === key; if (matchesKey) { (_a = t.onDismiss) === null || _a === void 0 ? void 0 : _a.call(t, timeoutExpired); } return !matchesKey; }); return { toastRefs: this.getToastRefs(toasts), toasts }; }); } clear() { var _a, _b; (_b = (_a = this.queue).cancel) === null || _b === void 0 ? void 0 : _b.call(_a); this.queue = { cancel: undefined, isRunning: false, toasts: [] }; this.state.toasts.forEach(t => { var _a; return (_a = t.onDismiss) === null || _a === void 0 ? void 0 : _a.call(t, false); }); this.setState({ toastRefs: {}, toasts: [] }); } getToasts() { return this.state.toasts; } render() { const classes = (0, classnames_1.default)(common_1.Classes.TOAST_CONTAINER, this.getPositionClasses(), this.props.className); return (React.createElement(overlay2_1.Overlay2, { autoFocus: this.props.autoFocus, canEscapeKeyClose: this.props.canEscapeKeyClear, canOutsideClickClose: false, className: classes, childRefs: this.toastRefs, enforceFocus: false, hasBackdrop: false, isOpen: this.state.toasts.length > 0 || this.props.children != null, onClose: this.handleClose, shouldReturnFocusOnClose: false, // $pt-transition-duration * 3 + $pt-transition-duration / 2 transitionDuration: 350, transitionName: common_1.Classes.TOAST, usePortal: this.props.usePortal }, this.state.toasts.map(this.renderToast, this), this.props.children)); } validateProps({ maxToasts }) { // maximum number of toasts should not be a number less than 1 if (maxToasts !== undefined && maxToasts < 1) { throw new Error(errors_1.TOASTER_MAX_TOASTS_INVALID); } } dismissIfAtLimit() { if (this.state.toasts.length === this.props.maxToasts) { // dismiss the oldest toast to stay within the maxToasts limit this.dismiss(this.state.toasts[this.state.toasts.length - 1].key); } } createToastOptions(props, key = `toast-${this.toastId++}`) { // clone the object before adding the key prop to avoid leaking the mutation return { ...props, key }; } getPositionClasses() { const positions = this.props.position.split("-"); // NOTE that there is no -center class because that's the default style return [ ...positions.map(p => `${common_1.Classes.TOAST_CONTAINER}-${p.toLowerCase()}`), `${common_1.Classes.TOAST_CONTAINER}-${this.props.usePortal ? "in-portal" : "inline"}`, ]; } } exports.OverlayToaster = OverlayToaster; OverlayToaster.displayName = `${props_1.DISPLAYNAME_PREFIX}.OverlayToaster`; OverlayToaster.defaultProps = { autoFocus: false, canEscapeKeyClear: true, position: common_1.Position.TOP, usePortal: true, }; //# sourceMappingURL=overlayToaster.js.map