@blueprintjs/core
Version:
Core styles & components
126 lines • 5.85 kB
JavaScript
/*
* Copyright 2022 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 * as React from "react";
import * as ReactDOM from "react-dom";
import { Classes, DISPLAYNAME_PREFIX } from "../../common";
import * as Errors from "../../common/errors";
import { PortalContext } from "../../context/portal/portalProvider";
/** @deprecated will be removed in Blueprint v6.0 */
const PORTAL_LEGACY_CONTEXT_TYPES = {
blueprintPortalClassName: (obj, key) => {
if (obj[key] != null && typeof obj[key] !== "string") {
return new Error(Errors.PORTAL_CONTEXT_CLASS_NAME_STRING);
}
return undefined;
},
};
/**
* Portal component.
*
* This component detaches its contents and re-attaches them to document.body.
* Use it when you need to circumvent DOM z-stacking (for dialogs, popovers, etc.).
* Any class names passed to this element will be propagated to the new container element on document.body.
*
* Portal supports both the newer React context API and the legacy context API.
* Support for the legacy context API will be removed in Blueprint v6.0.
*
* @see https://blueprintjs.com/docs/#core/components/portal
*/
export function Portal({ className, stopPropagationEvents, container, onChildrenMount, children }, legacyContext = {}) {
const context = React.useContext(PortalContext);
const portalContainer = container ?? context.portalContainer ?? document?.body;
const [portalElement, setPortalElement] = React.useState();
const createPortalElement = React.useCallback(() => {
const newPortalElement = document.createElement("div");
newPortalElement.classList.add(Classes.PORTAL);
maybeAddClass(newPortalElement.classList, className); // directly added to this portal element
maybeAddClass(newPortalElement.classList, context.portalClassName); // added via PortalProvider context
addStopPropagationListeners(newPortalElement, stopPropagationEvents);
// TODO: remove legacy context support in Blueprint v6.0
const blueprintPortalClassName = legacyContext.blueprintPortalClassName;
if (blueprintPortalClassName != null && blueprintPortalClassName !== "") {
console.error(Errors.PORTAL_LEGACY_CONTEXT_API);
maybeAddClass(newPortalElement.classList, blueprintPortalClassName); // added via legacy context
}
return newPortalElement;
}, [className, context.portalClassName, legacyContext.blueprintPortalClassName, stopPropagationEvents]);
// create the container element & attach it to the DOM
React.useEffect(() => {
if (portalContainer == null) {
return;
}
const newPortalElement = createPortalElement();
portalContainer.appendChild(newPortalElement);
setPortalElement(newPortalElement);
return () => {
removeStopPropagationListeners(newPortalElement, stopPropagationEvents);
newPortalElement.remove();
setPortalElement(undefined);
};
}, [portalContainer, createPortalElement, stopPropagationEvents]);
// wait until next successful render to invoke onChildrenMount callback
React.useEffect(() => {
if (portalElement != null) {
onChildrenMount?.();
}
}, [portalElement, onChildrenMount]);
React.useEffect(() => {
if (portalElement != null) {
maybeAddClass(portalElement.classList, className);
return () => maybeRemoveClass(portalElement.classList, className);
}
return undefined;
}, [className, portalElement]);
React.useEffect(() => {
if (portalElement != null) {
addStopPropagationListeners(portalElement, stopPropagationEvents);
return () => removeStopPropagationListeners(portalElement, stopPropagationEvents);
}
return undefined;
}, [portalElement, stopPropagationEvents]);
// Only render `children` once this component has mounted in a browser environment, so they are
// immediately attached to the DOM tree and can do DOM things like measuring or `autoFocus`.
// See long comment on componentDidMount in https://reactjs.org/docs/portals.html#event-bubbling-through-portals
if (typeof document === "undefined" || portalElement == null) {
return null;
}
else {
return ReactDOM.createPortal(children, portalElement);
}
}
Portal.displayName = `${DISPLAYNAME_PREFIX}.Portal`;
// eslint-disable-next-line deprecation/deprecation
Portal.contextTypes = PORTAL_LEGACY_CONTEXT_TYPES;
function maybeRemoveClass(classList, className) {
if (className != null && className !== "") {
classList.remove(...className.split(" "));
}
}
function maybeAddClass(classList, className) {
if (className != null && className !== "") {
classList.add(...className.split(" "));
}
}
function addStopPropagationListeners(portalElement, eventNames) {
eventNames?.forEach(event => portalElement.addEventListener(event, handleStopProgation));
}
function removeStopPropagationListeners(portalElement, events) {
events?.forEach(event => portalElement.removeEventListener(event, handleStopProgation));
}
function handleStopProgation(e) {
e.stopPropagation();
}
//# sourceMappingURL=portal.js.map