Skip to content

Flyout/Popover/DropdownMenu does not close on outside tap on iOS (useOnClickOutside relies on document "click") #643

Description

@dmitriisurdin

Environment

  • reshaped: 3.9.0
  • react / react-dom: 19.x
  • Affected: iOS Safari and iOS Chrome (both WebKit). Works on desktop (Chrome/Safari/Firefox) and Android.

Description

An open Flyout / Popover / DropdownMenu does not close when the user taps outside of it on iOS. The user gets stuck with the menu open and cannot interact with the page underneath. Selecting a menu item or re-tapping the trigger still closes it — only outside-tap dismissal is broken, and only on iOS.

Steps to reproduce

  1. On an iOS device (Safari or Chrome), render a DropdownMenu (or Popover) whose trigger sits among plain, non-interactive elements (e.g. a View/div without cursor: pointer).
  2. Tap the trigger to open the menu.
  3. Tap an empty / non-interactive area outside the menu.

Expected: the flyout closes (as it does on desktop / Android).
Actual: the flyout stays open.

Root cause

useOnClickOutside records the press position on mousedown / touchstart, but fires the actual close handler on a document-level click event:

// reshaped/dist/hooks/useOnClickOutside.js
document.addEventListener("mousedown", handleMouseDown, { passive: true });
document.addEventListener("touchstart", handleMouseDown, { passive: true });
// ...
const handleClick = (event) => {
  if ("button" in event && event.button === 2) return;
  if (isMouseDownInsideRef.current) return;
  handlerRef.current?.(event); // -> handleClose({ reason: "outside-click" })
};
document.addEventListener("click", handleClick);

Suggested fix

Trigger outside-dismissal from a pointer/touch event that iOS delivers regardless of target interactivity, instead of (or in addition to) click:

  1. Add a touchend listener alongside click. isMouseDownInsideRef is already set on touchstart, so the inside/outside bookkeeping keeps working:
document.addEventListener("click", handleClick);
document.addEventListener("touchend", handleClick); // iOS delivers this for any target
// ...remove both in cleanup
  1. Or switch outside detection to pointerdown (fires on iOS for all targets) and dismiss from there.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Awaiting / Blocked

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions