Safari bug with ios14 and scribble

Updating to ios14 (14.0.1) just gave me a painful reminder why I sometimes hate writing code for the web. As part of a fun web app I'm developing I'm including support for stylus input (particularly the Apple Pencil). Using the unified PointerEvent simplified a lot of my code and allowed me to support mouse / touch / stylus fairly easily.

So I update to ios14 and suddenly a large number of my PointerEvents on my iPad go missing. After debugging for a while I realized it seems related to the new "Scribble" feature of ios14. I boiled down the issue to the following simplified test case:

<!DOCTYPE html>
<html>
    <head>
        <style>
            body {
                touch-action: none;
                user-select: none;
            }
            #box {
                height: 500px;
                width: 500px;
                background-color: #999999;
                border: 1px solid #333;
                margin: 5em auto;
            }
        </style>
    </head>
    <body>
        <div id="box"></div>
        <script>
            const box = document.getElementById("box");
            const red = (e) => e.target.style.backgroundColor = "#FF0000";
            const gray = (e) => e.target.style.backgroundColor = "#999999";

            box.addEventListener("pointerdown", red, false);
            box.addEventListener("pointerup", gray, false);
    </script>
    </body>
</html>

Loading that page on an iPad and tapping with the pencil (fairly quickly) will show that a second tap quickly after the first won't register (try it here). If you keep tapping you'll see a pattern where every other tap goes missing. Disabling scribble (Settings -> Apple Pencil -> Scribble) on the iPad stops the behavior so it definitely seems related.

After a frustrating day of trying lots of different ways to fix it I finally came across a weird solution that seems to work. Things I tried that did not work: changing the event behavior for the pointerEvents - capturing, not capturing, passive, not passive, order of the events, ways of binding the events, preventing defaults etc... I tried using css to apply pointer-events: none to various parts of the application. Thinking that scribble was attempting to trigger a selection gesture, I tried using user-select: none. That also did not fix the issue.

So what fixed it? Adding a preventDefault to a touchmove event bound to the target (try with the hack). Wuh? I would think touch events wouldn't be affected by scribble since touch doesn't get fired by the stylus. Web development has gotten way more consistent with fewer hacks over the years, but there are still tons of these weird behaviors. I had flashbacks to developing for IE6 and all of the weird quirks you had to internalize to know how to get the damn thing to behave.

Anyway I hope this saves someone else the many hours it took me to figure this one out.