Parent portal closes when clicking inside of a nested portal
zhouzi opened this issue · 3 comments
This issue was originally discussed in #101, before the rewrite.
I created a bin reproducing it with the latest version of react-portal:
https://www.webpackbin.com/bins/-L2PNDWSpoEmiOUuzvFO
In v3, portal component listen to mouseup and touchstart events, but in v4 it listen to click event, which not always works fine for me.
https://github.com/tajo/react-portal/blob/3.x/lib/portal.js#L27
This can be fixed using event bubbling through the react-dom. The react doc has a chapter for this : https://reactjs.org/docs/portals.html#event-bubbling-through-portals
I personally created my own portal wrapper like this :
import uniqueId from 'lodash/uniqueId';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
class Portal extends Component {
componentDidMount() {
const { onClickOutside } = this.props;
if (!onClickOutside) return;
window.addEventListener('click', this.handleDocumentClick);
}
componentWillUnmount() {
if (this.defaultNode) {
document.body.removeChild(this.defaultNode);
}
this.defaultNode = null;
window.removeEventListener('click', this.handleDocumentClick);
}
id = this.props.id || uniqueId()
handleDocumentClick = (e) => {
if (!e.parentModals || !e.parentModals.includes(this.id)) {
this.props.onClickOutside(e);
}
}
handleClickInsideModal = (e) => {
const currentParentModals = e.nativeEvent.parentModals || [];
e.nativeEvent.parentModals = [
...currentParentModals,
this.id,
];
}
render() {
const { children } = this.props;
if (!this.defaultNode) {
this.defaultNode = document.createElement('div');
document.body.appendChild(this.defaultNode);
}
return ReactDOM.createPortal(
<div onClick={this.handleClickInsideModal}>
{children}
</div>,
this.defaultNode
);
}
}
Portal.propTypes = {
children: PropTypes.node,
onClickOutside: PropTypes.func,
id: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
])
};
export default Portal;
The onClick
event that react creates passes through nested Portal.
You can also nest portals in the DOM:
import React from "react";
import ReactDOM from "react-dom";
const PortalContext = React.createContext(
typeof document !== "undefined" ? document.body : null
);
export function Portal({ children }) {
// if it's a nested portal, context is the parent portal
// otherwise it's document.body
const context = React.useContext(PortalContext);
const [container] = React.useState(() => {
if (typeof document !== "undefined") {
const portal = document.createElement("div");
portal.className = "portal";
return portal;
}
// ssr
return null;
});
React.useLayoutEffect(() => {
if (container && context) {
context.appendChild(container);
return () => {
context.removeChild(container);
};
}
return undefined;
}, [container, context]);
if (container) {
const portal = ReactDOM.createPortal(children, container);
return (
<PortalContext.Provider value={container}>
{portal}
</PortalContext.Provider>
);
}
// ssr
return null;
}