tajo/react-portal

Opening other portals doesn't count as outside click

ranneyd opened this issue · 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).

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.

tajo commented

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

sontx commented

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.

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

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

Same issue as of v4.2.1

bump

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