Skip to content

Divi 5 Interactions: trigger:"click" handler in script-library-interactions.js listens to click only — no pointerup/touchend fallback, so close-buttons / tab-toggles / show-hide interactions are silently dead on mobile (verified on iOS Safari) #327

@s-a-s-k-i-a

Description

@s-a-s-k-i-a

Environment

  • Theme: Divi 5.5.2 (latest stable at the time of writing)
  • WordPress: 6.9.4
  • PHP: 8.4.20
  • Browser observed breaking: iOS Safari (iPhone, current iOS). Reported by the site's editor on a real device; reproduced through code review of the shipped bundle.
  • Site URL (private staging, basic-auth available on request): https://staging.tierheim-hannover.de — the Canvas popup containing the "Opening hours" content opens after 3 s on every page (auto-trigger on the global Theme-Builder header), and its close-X cannot be tapped on mobile. The same X works on desktop with a mouse click.

Summary

In Divi 5, an Interaction whose Trigger is set to "Click" never responds to taps on touch devices. The handler is registered as click only — there is no pointerup or touchend fallback — and the immediate preventDefault() + stopPropagation() inside the handler prevents the browser from cleanly synthesising a click after touchend on a generic <div> (which is what most Divi modules end up as on the front-end, including the Icon module used for popup close-buttons).

The visible effect on our site is that the Canvas popup with the site's opening-hours notice auto-opens on every page after 3 s, and its X close-button is unresponsive to taps. Users on phones cannot dismiss the overlay until they navigate to another page (where it opens again 3 s later).

This is generic to every Interaction with trigger:"click" — close-buttons, custom show/hide toggles, tab-like behaviour built with Interactions, etc. — not specific to Canvas popups.


Root cause (verified in shipped Divi 5.5.2 bundle)

wp-content/themes/Divi/includes/builder-5/visual-builder/build/script-library-interactions.js contains a switch (t.trigger) block that wires up listeners per trigger type. The "click" branch reads:

case "click":
  e instanceof HTMLElement && (e.style.cursor = "pointer"),
  S(e, "click", (t => {
    t.target?.closest(".et-vb-ui") || (
      t.preventDefault(),
      t.stopPropagation(),
      s(t.timeStamp)
    )
  }));
  break;

S(target, eventName, handler) is a thin wrapper around target.addEventListener(eventName, handler). Only "click" is registered. The other branches in the same switch are either desktop-only by definition (mouseEnter, mouseExit) or use observers (viewportEnter, viewportExit, breakpointEnter, breakpointExit), so the missing-touch problem is unique to the click branch.

Why click alone is insufficient on iOS Safari:

  1. The interaction target is usually a <div> (e.g. Icon module → et_pb_icon div), not a native <button> or <a>. iOS Safari emits a synthetic click event after touchend on generic divs only when the element looks clickable to the touch heuristic — usually because of cursor: pointer or role="button".
  2. The handler in the Divi bundle calls preventDefault() + stopPropagation() on the click event itself, which is fine — but the wrapping bundle also installs addEventListener("click", ..., true) (capture) handlers higher up that interact unpredictably with iOS's "click synthesis from touch" logic, especially when combined with the page-level scroll-handling listeners that Divi's other bundles add.
  3. Empirically: the close-X has cursor:pointer, is in the viewport, has pointer-events:auto, touch-action:auto, and is not occluded by any other element (verified with document.elementsFromPoint(centerX, centerY) — top of stack is the icon's own <span>). A real iPhone tap still does not dismiss the popup, while a synthetic el.click() from the Web Inspector console does (confirming the handler is wired up but never invoked from a tap).

Reproduction (3 minutes, no specific site required)

  1. Install Divi 5.5.2 (or any 5.x with the same interactions bundle).
  2. On any page, drop an Icon module and a Text module.
  3. Add a Divi 5 Interaction to the Icon: Trigger = Click, Effect = Toggle visibility, Target = the Text module.
  4. View the page on an iPhone (or any iOS device, Safari).
  5. Expected: tapping the icon toggles the text module.
  6. Actual: nothing happens. The icon has cursor: pointer, looks clickable, but no Interaction fires on tap.

A desktop mouse click works as expected, which masks the bug during builder testing.

We also reproduced the same dead-tap behaviour on:

  • Canvas popups (et_pb_canvas posts) where a close-X icon uses trigger:"click" with toggleVisibility — this is exactly our production case.
  • Standalone non-popup show/hide toggles. (i.e. it is not Canvas-specific.)

Suggested fix

Inside the case "click" branch of script-library-interactions.js, register a pointerup (or touchend) listener alongside the existing click listener, and de-duplicate when both fire in the same gesture:

case "click":
  if (e instanceof HTMLElement) {
    e.style.cursor = "pointer";
  }

  let lastSynthDispatch = 0;

  const runEffect = (timestamp) => {
    s(timestamp);
  };

  S(e, "click", (ev) => {
    if (ev.target?.closest(".et-vb-ui")) {
      return;
    }
    ev.preventDefault();
    ev.stopPropagation();
    runEffect(ev.timeStamp);
  });

  // Mobile / touch / pen fallback. On iOS Safari, click is not reliably
  // synthesised after touchend on a `<div>` once preventDefault() has been
  // called inside the click handler. pointerup runs in the same gesture
  // for touch + pen + mouse and lets us recover the behaviour.
  S(e, "pointerup", (ev) => {
    if (ev.pointerType !== "touch" && ev.pointerType !== "pen") {
      return; // mouse already worked via the click listener above.
    }
    if (ev.target?.closest(".et-vb-ui")) {
      return;
    }
    // If a real click did fire in the same gesture (some mobile browsers
    // do still emit one), skip — the click branch already handled it.
    // Track via lastSynthDispatch + a microtask-deferred check.
    const fireToken = performance.now();
    lastSynthDispatch = fireToken;
    queueMicrotask(() => {
      if (lastSynthDispatch !== fireToken) {
        return; // a later pointerup superseded us; bail.
      }
      runEffect(ev.timeStamp);
    });
  });
  break;

The cheapest possible change (if minimising churn matters more than perfect de-duplication) is just:

S(e, "click", clickHandler);
S(e, "pointerup", (ev) => {
  if (ev.pointerType === "touch" || ev.pointerType === "pen") {
    clickHandler(ev);
  }
});

— filtering on pointerType keeps the mouse path on the existing click listener (no behaviour change there), and only adds a tap/pen path.


Workaround in place (on my production site)

We ship a small mu-plugin (mu-plugins/30-tierheim-third-party-workarounds.php, section T9) that injects a footer inline script. It:

  1. Reads window.diviElementInteractionsData after DOMContentLoaded.
  2. For every entry with trigger === "click" and enabled !== false, finds all elements matching .et-interaction-trigger-{class} and binds a pointerup handler.
  3. The handler skips pointerType === "mouse" (already works upstream).
  4. It dispatches a synthetic new MouseEvent("click", { bubbles: true, cancelable: true }) on the element, which then runs through Divi's own click listener — so we don't have to duplicate Divi's effect logic.
  5. It uses a 350 ms idempotency window via a data-th-touch-click-fired-at attribute to suppress a second firing when the browser does eventually emit a native click after pointerup.
  6. It re-binds on the ETBuilderInteractionsUpdate event so newly rendered trigger nodes (e.g. from a Canvas replay) also receive the fallback.

This is a stop-gap. We will remove it as soon as Divi ships a touch-aware interactions runtime.


Why this is worth fixing on the Divi side

  1. Silent UX failure on mobile. Every site that has built any "click" Interaction (a popular feature surfaced prominently in the Visual Builder) is currently degrading on the largest share of real-world traffic — mobile. Site owners discover this only when an end-user complains.
  2. No public API change required. The fix is internal to one file (script-library-interactions.js), one switch branch.
  3. The pattern is well-established. pointerup + pointerType filtering is the canonical 2026-era way to bridge mouse + touch + pen with one code path. Divi 5 already targets evergreen browsers, so there is no compatibility cost.
  4. Easy to test. Any iOS/Android device with the repro above will demonstrate it. We are happy to verify a beta build on our staging environment.

Notes

  • We have not exhaustively surveyed which other interaction effects (cookie-set, attribute-toggle, preset-replace, …) are affected on top of toggleVisibility. The wiring problem is on the listener registration side — every effect that hangs off a trigger:"click" interaction is affected the same way.
  • We are NOT shipping touchend instead of pointerup in the workaround because pointerup cleanly handles pen + touch + mouse with one path, and is passive: true by default (no scroll-jank risk).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions