'Detect iOS pointer capture bug for polyfilling

As reported in the WebKit Bugilla, iOS 13-15 seems to have a bug when trying to use pointer capture on touch and stylus inputs. The problem is that calling setPointerCapture doesn't actually redirect future events when called on an element that was not the event's original target. However, calling hasPointerCapture will still work and return true as expected. Is there any way I would be able to feature test for this problem in order to implement a polyfill for it?

I created a demo for this problem on GitHub if it helps. The green and yellow divs will correctly track the pointer movement if the pointer starts on the respective element, but the orange div should act the same and the red div should never move. Both of the latter will still track the pointer as long as the pointer remains over the divs.

The code that I currently have for polyfilling the bug is as below. It works, but its detection procedure is a UA string test and therefore won't work in the other iOS browsers even though they share the same JavaScript engine.

// Need to figure out some way to test if this is needed.

if (navigator.userAgent.match(/Version\/1[345]\.\d+(?:\.\d+)? Safari/)) {
  const {
    setPointerCapture: set,
    hasPointerCapture: has,
    releasePointerCapture: release
  } = Element.prototype

  let targets = {},
      captures = {}

  Element.prototype.setPointerCapture = function setPointerCapture(pointerId) {
    if (pointerId in captures) {
      if (document.contains(this)) {
        captures[pointerId] = this
        return set.call(targets[pointerId], pointerId)
      } else {
        throw new TypeError("Element not in valid location")
      }
    } else {
      return set.call(this, pointerId)
    }
  }
  Element.prototype.hasPointerCapture = function hasPointerCapture(pointerId) {
    if (pointerId in captures) {
      return captures[pointerId] == this
    } else {
      return has.call(this, pointerId)
    }
  }
  Element.prototype.releasePointerCapture = function releasePointerCapture(pointerId) {
    if (pointerId in captures) {
      if (this.hasPointerCapture(pointerId)) {
        captures[pointerId] = null
        return release.call(targets[pointerId], pointerId)
      }
    } else {
      return release.call(this, pointerId)
    }
  }

  let registerPointer = function registerPointer(event) {
        if (event.pointerType == "touch" || event.pointerType == "pen") {
          targets[event.pointerId] = event.target
          captures[event.pointerId] = null
        }
      },
      redirectPointer = function redirectPointer(event) {
        if (captures[event.pointerId] != null && captures[event.pointerId] != event.target) {
          // Stop the original event
          event.preventDefault()
          event.stopPropagation()

          // Redispatch a new, cloned event
          captures[event.pointerId].dispatchEvent(new PointerEvent(event.type, event))
        }
      },
      redirectAndUnregisterPointer = function redirectAndUnregisterPointer(event) {
        redirectPointer(event)
        delete targets[event.pointerId]
        delete captures[event.pointerId]
      }

  addEventListener("pointerdown", registerPointer, {capture: true, passive: true})
  addEventListener("pointermove", redirectPointer, {capture: true, passive: false})
  addEventListener("pointerup", redirectAndUnregisterPointer, {capture: true, passive: false})
  addEventListener("pointercancel", redirectAndUnregisterPointer, {capture: true, passive: false})
}



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source