React: Portal Event Bubbling Use Cases

Created on 18 Aug 2020  Â·  3Comments  Â·  Source: facebook/react

This is a spillover from https://github.com/facebook/react/issues/11387.

The goal of this issue is not to argue about whether the current React's behavior makes sense in all situations. Rather, it is collect a list of use cases, both when the current behavior works well, and when it doesn't, so that they can inform the next iteration of the related APIs. We can't commit to any concrete timeframe on this at the moment, but a list like this will definitely reduce the amount of time that we'd need to spend to get up to speed on the problem space when we're ready to approach it.

If you'd like to contribute a use case, please comment with:

  • A name for your pattern (come up with something unique so we can refer to it later)
  • A brief description of the UI (but a screenshot is worth a thousand words)
  • A small CodeSandbox demo, if you want to make a stronger case
  • How React event bubbling behavior breaks (or helps) your case

    • Include any information about other pitfalls you encountered, be very specific

  • A workaround you are currently using, if any

    • If you tried some workarounds but they cause issues, let us know which ones

Please keep this thread on topic and let's keep general discussion in https://github.com/facebook/react/issues/11387. This is not a good thread for "+1" or requests to solve this faster — it's a thread for gathering research.

Thank you!

Discussion

Most helpful comment

Gonna toss the two typical cases we run into quickly here, and hopefully follow up with some actual code and more specific actual cases of workarounds or benefits as i think of places in our code bases that have em.

Canonical Modal case

For posterity I think this is probably the canonical "Why this is unhelpful" case, at least as far as we see in in issues via react-bootstrap.

function OpenModalButton() {
  const [show, setShow] = useState(false);
  return (
    <Button onClick={() => setShow((prev) => !prev)}>
      Open Me
      <Modal show={show} />
    </Button>
  );
}

This was more compelling before Fragment as it actively prevented encapsulating components naturally. This example does still cause pain when you need to rely on event bubbling further up. IME though that is usually easy to work around (editorializing: I think the workarounds are preferable patterns anyway). Cases where bubbling is depended on usually fall into two categories (as i've seen)

  • You care about the actual DOM hierarchy: workaround is using element.contains to check and filter out event (targets) that aren't relevant.
  • To avoiding prop drilling: workaround is pass a callback, use context

The Case where it's super helpful

Assume the below, CustomSelect renders a it's menu in a portal outside of the modal. As some backdrop, Modals should trap focus inside them, meaning as to tab through the modal body focus should cycle through the tabbable elements as if there was nothing else on the page below the modal. You don't want to accidentally focus an input underneath the modal.

That can be accomplished by ensuring focus/blur events are paired on the Modal. However, that relies on bubbling working Logically instead of "physically", e.g. moving focus to the Select menu _actually_ moves focus outside the Modal element, but semantically focus should be considered inside the modal. With the React bubbling you get that for free, and without it you need to whitelist any possible outside elements, a task that is impossible to generalize for libraries and hard to maintain.

function MyModal({ show }) {
  return (
    <Modal show>
      <div>
        <CustomSelect>
          <Option>Click</Option>
        </CustomSelect>
      </div>
    </Modal>
  );
}

There is one Practical rub with this, which is in practice implementing focus trapping often involves adding event handlers manually to refs, bypassing the React event system. This usually means you don't get to benefit from the React portal bubbling even when you want too, ref: react-focus-lock, react-overlays. This is more of a problem with react _missing_ another API, not the current behavior being bad.

All 3 comments

Gonna toss the two typical cases we run into quickly here, and hopefully follow up with some actual code and more specific actual cases of workarounds or benefits as i think of places in our code bases that have em.

Canonical Modal case

For posterity I think this is probably the canonical "Why this is unhelpful" case, at least as far as we see in in issues via react-bootstrap.

function OpenModalButton() {
  const [show, setShow] = useState(false);
  return (
    <Button onClick={() => setShow((prev) => !prev)}>
      Open Me
      <Modal show={show} />
    </Button>
  );
}

This was more compelling before Fragment as it actively prevented encapsulating components naturally. This example does still cause pain when you need to rely on event bubbling further up. IME though that is usually easy to work around (editorializing: I think the workarounds are preferable patterns anyway). Cases where bubbling is depended on usually fall into two categories (as i've seen)

  • You care about the actual DOM hierarchy: workaround is using element.contains to check and filter out event (targets) that aren't relevant.
  • To avoiding prop drilling: workaround is pass a callback, use context

The Case where it's super helpful

Assume the below, CustomSelect renders a it's menu in a portal outside of the modal. As some backdrop, Modals should trap focus inside them, meaning as to tab through the modal body focus should cycle through the tabbable elements as if there was nothing else on the page below the modal. You don't want to accidentally focus an input underneath the modal.

That can be accomplished by ensuring focus/blur events are paired on the Modal. However, that relies on bubbling working Logically instead of "physically", e.g. moving focus to the Select menu _actually_ moves focus outside the Modal element, but semantically focus should be considered inside the modal. With the React bubbling you get that for free, and without it you need to whitelist any possible outside elements, a task that is impossible to generalize for libraries and hard to maintain.

function MyModal({ show }) {
  return (
    <Modal show>
      <div>
        <CustomSelect>
          <Option>Click</Option>
        </CustomSelect>
      </div>
    </Modal>
  );
}

There is one Practical rub with this, which is in practice implementing focus trapping often involves adding event handlers manually to refs, bypassing the React event system. This usually means you don't get to benefit from the React portal bubbling even when you want too, ref: react-focus-lock, react-overlays. This is more of a problem with react _missing_ another API, not the current behavior being bad.

This is super helpful for suppressing outside click/focus in/focus out events from portalled content and can really help simplify project architecture.

Aside from the case mentioned like Tooltips, Selects etc, it can also be used to help avoid global state for Sidebar UIs because property panel code can be colocated with the thing they're configuring and outside click events can be prevented when interacting with these property panels:

https://codesandbox.io/s/collocate-concerns-with-portals-yrjkc

pr

Clicking outside the button exits the button editing mode but interacting with the property panel does not as it is inside the ButtonEditor React tree. I did not have to use any complex logic for this.

In my humble opinion, if people would like to bind events that respect the DOM tree only then it would make sense for the code to explicitly bind to DOM events. It would more clearly communicate the intent.

Just popping the Tooltip case here too where refocusing the menu button after the menu closes should not reactivate its tooltip (use keyboard, space to open/close - also desktop only, it's PoC code 😅).

https://codesandbox.io/embed/usefocusenter-forked-x6i0n

I can continue posting more but the general pattern is the ability to prevent events from portalled content as it is considered _inside_. Is it worth me posting more use-cases for this?

Was this page helpful?
0 / 5 - 0 ratings