-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ClickAwayListener] Handle portaled element #18586
Comments
Your problem is related to a "couple" of others 😆:
|
We have a full reproduction example in #17990 that we can leverage. Based on it, I could come up with this potential solution. I would propose the following fix, let me know what you think about it: diff --git a/packages/material-ui/src/ClickAwayListener/ClickAwayListener.js b/packages/material-ui/src/ClickAwayListener/ClickAwayListener.js
index c689731b6..8e38b6635 100644
--- a/packages/material-ui/src/ClickAwayListener/ClickAwayListener.js
+++ b/packages/material-ui/src/ClickAwayListener/ClickAwayListener.js
@@ -16,10 +16,17 @@ function mapEventPropToEvent(eventProp) {
* For instance, if you need to hide a menu when people click anywhere else on your page.
*/
const ClickAwayListener = React.forwardRef(function ClickAwayListener(props, ref) {
- const { children, mouseEvent = 'onClick', touchEvent = 'onTouchEnd', onClickAway } = props;
+ const {
+ children,
+ disableReactTree = false,
+ mouseEvent = 'onClick',
+ onClickAway,
+ touchEvent = 'onTouchEnd',
+ } = props;
const movedRef = React.useRef(false);
const nodeRef = React.useRef(null);
const mountedRef = React.useRef(false);
+ const syntheticEvent = React.useRef(false);
React.useEffect(() => {
mountedRef.current = true;
@@ -40,6 +47,9 @@ const ClickAwayListener = React.forwardRef(function ClickAwayListener(props, ref
const handleRef = useForkRef(children.ref, handleOwnRef);
const handleClickAway = useEventCallback(event => {
+ const insideReactTree = syntheticEvent.current;
+ syntheticEvent.current = false;
+
// Ignore events that have been `event.preventDefault()` marked.
if (event.defaultPrevented) {
return;
@@ -67,7 +77,8 @@ const ClickAwayListener = React.forwardRef(function ClickAwayListener(props, ref
if (
doc.documentElement &&
doc.documentElement.contains(event.target) &&
- !nodeRef.current.contains(event.target)
+ !nodeRef.current.contains(event.target) &&
+ (disableReactTree || !insideReactTree)
) {
onClickAway(event);
}
@@ -106,7 +117,39 @@ const ClickAwayListener = React.forwardRef(function ClickAwayListener(props, ref
return undefined;
}, [handleClickAway, mouseEvent]);
- return <React.Fragment>{React.cloneElement(children, { ref: handleRef })}</React.Fragment>;
+ const handleSyntheticMouse = event => {
+ syntheticEvent.current = true;
+
+ const childrenProps = children.props;
+ if (childrenProps[mouseEvent]) {
+ childrenProps[mouseEvent](event);
+ }
+ };
+
+ const handleSyntheticTouch = event => {
+ syntheticEvent.current = true;
+
+ const childrenProps = children.props;
+ if (childrenProps[touchEvent]) {
+ childrenProps[touchEvent](event);
+ }
+ };
+
+ const childrenProps = {};
+
+ if (mouseEvent !== false) {
+ childrenProps[mouseEvent] = handleSyntheticMouse;
+ }
+
+ if (touchEvent !== false) {
+ childrenProps[touchEvent] = handleSyntheticTouch;
+ }
+
+ return (
+ <React.Fragment>
+ {React.cloneElement(children, { ref: handleRef, ...childrenProps })}
+ </React.Fragment>
+ );
});
ClickAwayListener.propTypes = {
@@ -114,6 +157,11 @@ ClickAwayListener.propTypes = {
* The wrapped element.
*/
children: elementAcceptingRef.isRequired,
+ /**
+ * If `true`, the React tree is ignored and only the DOM tree is considered.
+ * This prop changes how portaled elements are handled.
+ */
+ disableReactTree: PropTypes.bool,
/**
* The mouse event to listen to. You can disable the listener by providing `false`.
*/ cc @Izhaki as it might impact your hook API |
I don't think this is a good first issue. This discussion should happen in the react core. Special casing where we want to ignore the component tree for event bubbling is (as with most special cases) not a good idea. We should rather explore alternative APIs or contribute these use cases to the appropriate issues in the react repositories. Otherwise we introduce more suprises and bundle size into the API. |
@eps1lon Do you mean that we shouldn't expose a Yeah, I think that we can wait to see a compelling use case for this prop 👍. Including the React tree should likely be the default behavior, and changed, independently from the DOM tree. |
I need ClickAwayListener to intercept a And the idea of having a dependency in The point I'm trying to make is that this functionality is possibly far more generic to be included in MUI. I'd consider just extracting it to its own package (I've seen at least 3 bespoke implementation of it already in other libraries, who would probably be delighted to see a package for it. currently one needs to have come across mui to know it exist). |
@Izhaki What makes you uncomfortable about it? Material-UI's primary mission is to bring material for developers to build UIs. Material Design is our default theme. |
I'm writing a library that has nothing to do with mui. I just need ClickAwayListener. |
@Izhaki Would you feel better with a |
Yes! (and No!) I mean - all things are equal tree-shaking wise. But it's different having dependency on But it's a bit of rabbit hole really... why do this for So you can make a case for |
@Izhaki I'm trying to navigate with your fears and the scope of our options. Exposing all our modules as packages could be interesting, especially with unstyled components. |
I have updated my comment. I don't really have a firm solution for this. But I do think there's a lot in this project that would be useful for people who don't use |
@Izhaki I was recently thinking of another aspect of this problem: the documentation. I was considering adding a link to sources in the docs page (had the idea from Reach) and to automatically pull the bundle size info from (https://material-ui.com/size-snapshot), rather than hard coding it in the markdown. I think that it would reduce people's fears of bloatness and to better audit the quality of what they are "buying" (using). |
Ok so after @eps1lon's recommendation:
|
Hi. I thought I would chime in on this. I think the scope of my issue has changed from popper positioning issues to clickaway listener issues? My main concern was the positioning of the popup menu when using a transform (similar to how popper works). Popover does not use a position transform but instead uses absolute positioning. The popover works correctly with a select dropdown field. See my original codesandbox for the example. |
@ccmartell We are going after the root cause. |
Hi,
|
Hey folks! I was about to open a dupe of this issue. In case you're looking for more repros or test cases, here's my scenario in which
If you click the Select, it fires the |
@victorhurdugaci Do you want to complete the proposed patch in #18586 (comment) with a pull request :)? |
I’ll try take a look this weekend unless someone else gets to it before me :) I know that you’ve provided a diff to apply but would like to get familiar with the underlying problem first @oliviertassinari |
Hi, I am sorry to continue commenting on this closed PR, but I just tried the disableReactTree props, while it fixed to don't trigger onClickAway from ClickAwayListener, it still has some issues when using Select inside a ClickAwayListener wrapped by Popper. I have made the demo here https://codesandbox.io/s/material-demo-89jyg?file=/demo.js. But if I use |
@pawlarius What problem to you try to solve? |
@pawlarius I'm seeing the same issue in my project. I passed a classes prop to <Select
MenuProps={{
classes: { paper: classes.selectPaper },
disablePortal: true
}}
>
<MenuItem value={10}>Ten</MenuItem>
</Select>
const styles = theme => ({
selectPaper: {
// hack to fix Material UI popover position when disablePortal is true
left: `${theme.spacing(2)}px !important`,
maxHeight: 'none !important',
top: `${theme.spacing(2)}px !important`
}
}) |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Do you have a suggestion for the zIndex positioning? for some reason, for the life of me, I can't figure out how to solve this issue of the component ghostly hovering over the other, which can be clicked through as well. |
@showduhtung v5 |
@oliviertassinari Hmm, I tested this test snippet in https://github.com/NMinhNguyen/material-ui/commit/3b7a19f50577ede28223375047ebab23c613bf02 and it works, but it doesn't seem to work the same with components. I haven't tested for all other portal using components, but as you can see in my sandbox, while the Portal is being acknowledged within ClickAwayListener, seems to still be a portal as it's still triggering ClickAwayListener. Sandbox: https://codesandbox.io/s/wild-wood-i7d7g?file=/src/App.js |
@showduhtung You need to test with v5, but in any case, I can reproduce the click away that triggers when interacting with a select: https://codesandbox.io/s/quirky-aryabhata-9btw1?file=/src/App.js. It seems that the click event can't find a target and fallback to the diff --git a/packages/material-ui/src/ClickAwayListener/ClickAwayListener.js b/packages/material-ui/src/ClickAwayListener/ClickAwayListener.js
index 735c990f09..fc862ee5b7 100644
--- a/packages/material-ui/src/ClickAwayListener/ClickAwayListener.js
+++ b/packages/material-ui/src/ClickAwayListener/ClickAwayListener.js
@@ -73,6 +73,11 @@ function ClickAwayListener(props) {
return;
}
+ // The click event couldn't find its target, ignore
+ if (event.target.nodeName === 'BODY' && event.type === 'click') {
+ return;
+ }
+
let insideDOM;
// If not enough, can use https://github.com/DieterHolvoet/event-propagation-path/blob/master/propagationPath.js Is this the problem you are facing? Do you want to open a new issue for it so we can continue the exploration? An alternative solution would be to determine the click outside during the mouse down event and delay the trigger. |
Current Behavior 😯
When a select component is within a popper element and disablePortal is true, the dropdown items are positioned absolutely in relation to the popper element and not the page. This is also true for any absolutely positioned container that uses transform (which is what popper is using).
Expected Behavior 🤔
Expected behavior would be that the position of the dropdown would match the location of the select element and display correctly.
Steps to Reproduce 🕹
I have mocked the issue here
Steps:
Context 🔦
When using a clickaway listener with popper to display a popup, a portaled menu will cause the clickaway listener to fire, closing the popper and losing our work. We then were able to use disablePortal in the MenuProps, but that causes this issue. We have done a workaround using Popover as it doesn't have a transform and displays the Select menu correctly.
Your Environment 🌎
The text was updated successfully, but these errors were encountered: