@blueprintjs/core
Version:
Core styles & components
292 lines • 14.4 kB
JavaScript
/*
* 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