react-portal icon indicating copy to clipboard operation
react-portal copied to clipboard

Opening other portals doesn't count as outside click

Open ranneyd opened this issue 6 years ago • 8 comments

My problem

I have a situation where I have a list of items with little arrows for dropdowns that are going in portals. I'm using PortalWithState with closeOnOutsideClick for the dropdowns, and buttons with onClick={openPortal} as the arrows. When I have one of them open and I outside click it usually works fine, but when I click on one of the other arrows, it opens that portal without closing the open one.

My theory:

In every scenario I've encountered, custom click-events are resolved after react ones, so the click on the arrow should resolve first. That click triggers openPortal, which does e.nativeEvent.stopImmediatePropagation(). I think that's stopping the propagation that would trigger the outside click listener.

My (gross) solution:

This is a workaround that currently works for my current use case.

const fakeEvent = {
  nativeEvent: {
    stopImmediatePropagation: () => {},
  },
};
setTimeout(() => openPortal(fakeEvent), 0);

The setTimeout forces the portal opening to the bottom of the queue (so the outside click resolves first). However, React recycles the event handler, so I can't pass it directly to openPortal. I have to pass this mock one that doesn't actually do anything so I don't get errors for things being undefined. This currently does what I want (closes the current dropdown and opens the other one).

ranneyd avatar Nov 07 '17 23:11 ranneyd

NOTE: I've discovered that this makes clicking on the arrow for the original dropdown reopen the portal immediately after it closes. I have to work around that by only binding my toggle event when !isOpen is true.

ranneyd avatar Nov 07 '17 23:11 ranneyd

Any idea how to fix this and keep the current functionality (not reopening portal when you click on the button) ?

tajo avatar Nov 14 '17 18:11 tajo

I think we don't need to pass an event to openPortal anymore, use a "opening" flag

function openPortal() {
      var _this1 = this;
      if (_this1.state.active) {
        return;
      }
      // turn the "opening" flag to true to prevent handleOutsideMouseClick method close the portal
      _this1.setState({ active: true, opening: true }, _this1.props.onOpen);
      // turn the "opening" flag to false it means the portal already opened, so the handleOutsideMouseClick method can handle again
      setTimeout(function () {
	 _this1.setState({ active: true, opening: false })
      }, 0);
}
function handleOutsideMouseClick(e) {
      if (!this.state.active || this.state.opening) {// if we are opening the portal, so we must ignore click outside event
        return;
      }
      var root = findDOMNode(this.portalNode);
      if (!root || root.contains(e.target) || e.button && e.button !== 0) {
        return;
      }
      this.closePortal();
}

It just a trick but it works for me :D, sorry for my bad english and my "built code" because I just modified the "built" code insteads of source code.

sontx avatar Nov 20 '17 10:11 sontx

stopPropagation should always be considered harmful, because other components on a page cannot react to a click anymore.

webholics avatar Feb 21 '18 14:02 webholics

So, I'm facing this same issue any idea?

mnmistake avatar Jul 15 '19 03:07 mnmistake

Same issue as of v4.2.1

Frondor avatar Feb 18 '20 21:02 Frondor

bump

alekslario avatar Jun 30 '20 15:06 alekslario

Couldn't find a solution to this issue so I used a global redux/context store + custom hook fallback.

import React, { useEffect, useState } from "react";
import { useStore } from "./contextStore";
import shortid from "shortid";
export const useCloseModals = (closePortal) => {
  const [store, dispatch] = useStore();
  const [id] = useState(shortid.generate());

  useEffect(() => {
    const handle = setTimeout(() => {
      if (store.activeModal !== id) closePortal();
    }, 0);
    return () => clearTimeout(handle);
  }, [store.activeModal]);

  useEffect(() => {
    dispatch({ type: "SET_ACTIVE_MODAL", id });
  }, []);
};

Works well

alekslario avatar Jun 30 '20 21:06 alekslario