Skip to content

[Ionic Migration] Pull-to-refresh haptic feedback does not work on iOS Safari #544

@sahilc0

Description

@sahilc0

Problem

The pull-to-refresh implementation has haptic feedback coded to fire when the user crosses the pull threshold, but haptics do not trigger on iOS Safari despite multiple implementation approaches.

Button haptics in the same app work correctly using the same triggerHaptic() function from src/lib/haptics.ts.

Technical Details

Haptic Library

  • Using web-haptics library (https://haptics.lochie.me/)
  • On iOS Safari, it uses a "hidden switch hack" - programmatically clicking a hidden <input type="checkbox"> element which triggers the Taptic Engine
  • This works for button onClick handlers but NOT for touch gesture handlers

Root Cause (Best Understanding)

Per Codex GPT-5.4 analysis:

"On iOS Safari it synthesizes haptics by programmatically clicking a hidden switch. That means the call has to stay inside Safari's trusted tap/click activation path; a custom drag gesture is easier to fall outside of that than a button onClick."

The web-haptics library's click-a-hidden-switch approach only works inside Safari's "trusted gesture activation path." Custom drag gestures (touchmove) appear to fall outside this trust chain, even when:

  • Using passive: false on the event listener
  • Calling e.preventDefault() to stay in an active gesture context
  • Firing the haptic synchronously during the touch event

Approaches Attempted (All Failed)

  1. Haptic in touchmove with passive: false + preventDefault()

    • Expected: Should maintain trusted gesture context
    • Result: No haptic
  2. Haptic in touchmove before preventDefault()

    • Expected: Fire haptic before canceling default behavior
    • Result: No haptic
  3. Using setTimeout(() => triggerHaptic(), 0)

    • Expected: Decouple from touch event context
    • Result: No haptic
  4. Using navigator.vibrate() directly

    • Result: Not supported on iOS Safari at all
  5. Haptic on touchend instead of touchmove

    • Expected: touchend is similar to click context
    • Result: No haptic
  6. All passive handlers with CSS touch-action: none

    • Expected: Avoid preventDefault issues entirely
    • Result: No haptic
  7. Using pulltorefreshjs library

    • Result: Library handles its own UI (ugly), haptics still don't work

Current Implementation

The PTR is in src/hooks/usePullToRefresh.ts and works correctly for:

  • Visual feedback (content translates down with rubber-band physics)
  • Nested scroll detection
  • Direction lock (ignores horizontal swipes)
  • Calling the refresh callback

Only haptic feedback is broken.

Relevant Files

  • src/hooks/usePullToRefresh.ts - PTR hook with haptic calls
  • src/lib/haptics.ts - Haptic module using web-haptics
  • src/components/Button.tsx - Working haptics (for reference)
  • src/components/Content.tsx - Uses the PTR hook

Possible Solutions to Investigate

  1. Different haptic library - Is there one that uses a different iOS Safari hack?

  2. Native bridge - If this becomes a Capacitor app, use native haptics API

  3. Accept limitation - Only fire haptics on refresh completion (after the async callback), not on threshold crossing

  4. Apple WebKit bug/feature request - The Taptic Engine API restriction to "trusted" gestures may be intentional for battery/UX reasons

Environment

  • iOS Safari (iPhone)
  • web-haptics v0.1.x
  • React 18
  • Vite

Related

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