UNPKG

@blueprintjs/core

Version:

Core styles & components

292 lines 14.4 kB
/* * 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. */ import { __assign, __extends, __spreadArray } from "tslib"; import classNames from "classnames"; import * as React from "react"; import * as ReactDOM from "react-dom"; import { AbstractPureComponent, Classes, Position } from "../../common"; import { TOASTER_CREATE_ASYNC_NULL, TOASTER_CREATE_NULL, TOASTER_MAX_TOASTS_INVALID, TOASTER_WARN_INLINE, } from "../../common/errors"; import { DISPLAYNAME_PREFIX } from "../../common/props"; import { isElementOfType, isNodeEnv } from "../../common/utils"; import { Overlay2 } from "../overlay2/overlay2"; import { Toast } from "./toast"; import { Toast2 } from "./toast2"; export var OVERLAY_TOASTER_DELAY_MS = 50; /** * OverlayToaster component. * * @see https://blueprintjs.com/docs/#core/components/toast */ var OverlayToaster = /** @class */ (function (_super) { __extends(OverlayToaster, _super); function OverlayToaster() { var _this = _super !== null && _super.apply(this, arguments) || this; _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 = function (toasts) { return toasts.reduce(function (refs, toast) { refs[toast.key] = React.createRef(); return refs; }, {}); }; _this.handleQueueTimeout = function () { var nextToast = _this.queue.toasts.shift(); if (nextToast != null) { _this.immediatelyShowToast(nextToast); _this.startQueueTimeout(); } else { _this.queue.isRunning = false; } }; _this.renderToast = function (toast) { return React.createElement(Toast2, __assign({}, toast, { onDismiss: _this.getDismissHandler(toast) })); }; _this.getDismissHandler = function (toast) { return function (timeoutExpired) { _this.dismiss(toast.key, timeoutExpired); }; }; _this.handleClose = function (e) { // NOTE that `e` isn't always a KeyboardEvent but that's the only type we care about if (e.key === "Escape") { _this.clear(); } }; return _this; } /** * 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. */ OverlayToaster.create = function (props, container) { if (container === void 0) { container = document.body; } if (props != null && props.usePortal != null && !isNodeEnv("production")) { console.warn(TOASTER_WARN_INLINE); } var containerElement = document.createElement("div"); container.appendChild(containerElement); // TODO(React 18): Replace deprecated ReactDOM methods. See: https://github.com/palantir/blueprint/issues/7166 // eslint-disable-next-line @typescript-eslint/no-deprecated var toaster = ReactDOM.render(React.createElement(OverlayToaster, __assign({}, props, { usePortal: false })), containerElement); if (toaster == null) { throw new Error(TOASTER_CREATE_NULL); } return toaster; }; /** * Similar to {@link OverlayToaster.create}, but returns a Promise to a * Toaster instance after it's rendered and mounted to the DOM. * * This API will replace the synchronous {@link OverlayToaster.create} in a * future major version of Blueprint to reflect React 18+'s new asynchronous * rendering API. */ OverlayToaster.createAsync = function (props, options) { var _a, _b; if (props != null && props.usePortal != null && !isNodeEnv("production")) { console.warn(TOASTER_WARN_INLINE); } var container = (_a = options === null || options === void 0 ? void 0 : options.container) !== null && _a !== void 0 ? _a : document.body; // TODO(React 18): Replace deprecated ReactDOM methods. See: https://github.com/palantir/blueprint/issues/7166 // eslint-disable-next-line @typescript-eslint/no-deprecated var domRenderer = (_b = options === null || options === void 0 ? void 0 : options.domRenderer) !== null && _b !== void 0 ? _b : ReactDOM.render; var toasterComponentRoot = document.createElement("div"); container.appendChild(toasterComponentRoot); return new Promise(function (resolve, reject) { try { // TODO(React 18): Replace deprecated ReactDOM methods. See: https://github.com/palantir/blueprint/issues/7166 // eslint-disable-next-line @typescript-eslint/no-deprecated domRenderer(React.createElement(OverlayToaster, __assign({}, 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(TOASTER_CREATE_ASYNC_NULL)); return; } resolve(ref); } }); }; OverlayToaster.prototype.show = function (props, key) { var options = this.createToastOptions(props, key); var 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; }; OverlayToaster.prototype.maybeUpdateExistingToast = function (options, key) { if (key == null) { return false; } var isExistingQueuedToast = this.queue.toasts.some(function (toast) { return toast.key === key; }); if (isExistingQueuedToast) { this.queue.toasts = this.queue.toasts.map(function (t) { return (t.key === key ? options : t); }); return true; } var isExistingShownToast = this.state.toasts.some(function (toast) { return toast.key === key; }); if (isExistingShownToast) { this.updateToastsInState(function (toasts) { return toasts.map(function (t) { return (t.key === key ? options : t); }); }); return true; } return false; }; OverlayToaster.prototype.immediatelyShowToast = function (options) { if (this.props.maxToasts) { // check if active number of toasts are at the maxToasts limit this.dismissIfAtLimit(); } this.updateToastsInState(function (toasts) { return __spreadArray([options], toasts, true); }); }; OverlayToaster.prototype.startQueueTimeout = function () { this.queue.isRunning = true; this.queue.cancel = this.setTimeout(this.handleQueueTimeout, OVERLAY_TOASTER_DELAY_MS); }; OverlayToaster.prototype.updateToastsInState = function (getNewToasts) { var _this = this; this.setState(function (prevState) { var toasts = getNewToasts(prevState.toasts); return { toastRefs: _this.getToastRefs(toasts), toasts: toasts }; }); }; OverlayToaster.prototype.dismiss = function (key, timeoutExpired) { var _this = this; if (timeoutExpired === void 0) { timeoutExpired = false; } this.setState(function (prevState) { var toasts = prevState.toasts.filter(function (t) { var _a; var 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: toasts }; }); }; OverlayToaster.prototype.clear = function () { 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(function (t) { var _a; return (_a = t.onDismiss) === null || _a === void 0 ? void 0 : _a.call(t, false); }); this.setState({ toastRefs: {}, toasts: [] }); }; OverlayToaster.prototype.getToasts = function () { return this.state.toasts; }; OverlayToaster.prototype.render = function () { var classes = classNames(Classes.TOAST_CONTAINER, this.getPositionClasses(), this.props.className); return (React.createElement(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: Classes.TOAST, usePortal: this.props.usePortal }, this.state.toasts.map(this.renderToast, this), this.renderChildren())); }; OverlayToaster.prototype.validateProps = function (_a) { var maxToasts = _a.maxToasts; // maximum number of toasts should not be a number less than 1 if (maxToasts !== undefined && maxToasts < 1) { throw new Error(TOASTER_MAX_TOASTS_INVALID); } }; /** * If provided `Toast` children, automaticaly upgrade them to `Toast2` elements so that `Overlay2` can inject * refs into them for use by `CSSTransition`. This is a bit hacky but ensures backwards compatibility for * `OverlayToaster`. It should be an uncommon code path in most applications, since we expect most usage to * occur via the imperative toaster APIs. * * We can remove this indirection once `Toast2` fully replaces `Toast` in a future major version. * * TODO(@adidahiya): Blueprint v6.0 */ OverlayToaster.prototype.renderChildren = function () { return React.Children.map(this.props.children, function (child) { // TODO(React 18): Replace deprecated ReactDOM methods. See: https://github.com/palantir/blueprint/issues/7166 // eslint-disable-next-line @typescript-eslint/no-deprecated if (isElementOfType(child, Toast)) { return React.createElement(Toast2, __assign({}, child.props)); } else { return child; } }); }; OverlayToaster.prototype.dismissIfAtLimit = function () { 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); } }; OverlayToaster.prototype.createToastOptions = function (props, key) { if (key === void 0) { key = "toast-".concat(this.toastId++); } // clone the object before adding the key prop to avoid leaking the mutation return __assign(__assign({}, props), { key: key }); }; OverlayToaster.prototype.getPositionClasses = function () { var positions = this.props.position.split("-"); // NOTE that there is no -center class because that's the default style return __spreadArray(__spreadArray([], positions.map(function (p) { return "".concat(Classes.TOAST_CONTAINER, "-").concat(p.toLowerCase()); }), true), [ "".concat(Classes.TOAST_CONTAINER, "-").concat(this.props.usePortal ? "in-portal" : "inline"), ], false); }; OverlayToaster.displayName = "".concat(DISPLAYNAME_PREFIX, ".OverlayToaster"); OverlayToaster.defaultProps = { autoFocus: false, canEscapeKeyClear: true, position: Position.TOP, usePortal: true, }; return OverlayToaster; }(AbstractPureComponent)); export { OverlayToaster }; //# sourceMappingURL=overlayToaster.js.map