Skip to content

Spatial UI forwarded touch/click events lose iOS Safari user activation #41

Description

@felixtrz

Summary

On iOS Safari, world-space Spatial UI buttons receive forwarded UIKit/pointer events correctly, but actions that require browser user activation can fail when run from the forwarded pointerup/click handler.

The visible UI state updates, pointerenter, pointerdown, pointerup, and click handlers all fire on the UIKit element, but browser-gated APIs such as window.open() are not reliably treated as being inside the native Safari touch activation. Running the same action from a native DOM pointerup capture listener on the canvas/window succeeds.

This makes it look like forwardHtmlEvents is preserving the interaction semantically, but not preserving the native user-activation timing needed by iOS Safari.

Environment

  • @iwsdk/core: 0.3.2
  • Three.js: r181 through IWSDK
  • @pmndrs/uikit: 1.0.64
  • Browser: iOS Safari
  • Mode: non-immersive browser mode, WebGL canvas, world-space PanelUI
  • Spatial UI config includes features.spatialUI.forwardHtmlEvents: true

Minimal shape of the setup

World.create(container, {
  xr: { /* normal WebXR config */ },
  features: {
    spatialUI: {
      forwardHtmlEvents: true,
    },
  },
});

// A world-space PanelUI entity loads UIKit markup with a button-like element.
const button = document.getElementById('test-button');

button.addEventListener('pointerenter', () => {
  // Visual hover/pressed affordance works on iOS Safari.
});

button.addEventListener('pointerup', () => {
  window.open('https://example.com', '_blank', 'noopener,noreferrer');
});

button.addEventListener('click', () => {
  window.open('https://example.com', '_blank', 'noopener,noreferrer');
});

Actual behavior

On iOS Safari:

  1. Touching the UIKit element visibly triggers the hover/press affordance.
  2. UIKit receives pointerdown, pointerup, and click for the button.
  3. The handler runs.
  4. window.open() does not reliably open a new tab/window.
  5. There may be no obvious app-level exception or console error from the handler.

A sanitized event trace from one tap looked like this:

ui     pointerenter pointerType=screen-touch buttons=1 target=test-button
native pointerdown  pointerType=touch        buttons=1 target=canvas
native touchstart   target=canvas
ui     pointerdown  pointerType=screen-touch buttons=1 target=test-button
native pointerup    pointerType=touch        buttons=0 target=canvas
native touchend     target=canvas
ui     pointerup    pointerType=screen-touch buttons=0 target=test-button
ui     click        pointerType=screen-touch buttons=0 target=test-button
ui     pointerleave pointerType=screen-touch buttons=0 target=test-button
native click        pointerType=mouse        buttons=0 target=canvas

The important part is that the native DOM pointerup occurs before the forwarded UIKit pointerup/click. Browser user activation appears to still be valid in the native DOM event, but not by the time the forwarded UIKit event handler runs.

Expected behavior

A tap on a forwarded Spatial UI button should allow common user-activation-dependent actions to run from the button handler on iOS Safari, especially:

  • window.open(...)
  • entering or launching an XR/session flow where the browser requires a user gesture
  • similar browser-gated APIs that need to run during native activation

Workaround that succeeds

A local workaround is to bridge native touch events manually:

  1. Track the currently hovered UIKit button from forwarded pointerenter where pointerType includes screen-touch.
  2. Add native window capture listeners for DOM pointerdown/pointerup where event.pointerType === 'touch'.
  3. On native DOM pointerup, if the pointer is still within tap movement/time thresholds, run the mapped button action immediately.
  4. Suppress the later forwarded UIKit pointerup/click duplicate.

That makes window.open() work on iOS Safari, which strongly suggests this is an activation-timing issue rather than hit testing or event delivery.

Possible cause

Looking at the event flow, this may be related to forwarded HTML events being batched and emitted later from the Spatial UI update pass rather than synchronously during the native DOM pointer event. iOS Safari is strict about user activation being consumed in the native event task.

A possible SDK-level fix might be one of:

  • Dispatch forwarded HTML pointer/click events synchronously for activation-sensitive events.
  • Expose a batchEvents: false option through IWSDK's spatialUI.forwardHtmlEvents configuration.
  • Provide a first-class Spatial UI activation event/hook that runs during the native DOM touch event while still resolving to the hit UIKit element.

Why this matters

The current behavior is confusing because the button visibly reacts and the UIKit handler fires, so app code appears correct. The failure only shows up when the handler uses a browser-gated API on iOS Safari.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions