The report says the page scrolled while they were dragging the slider. Or that pull-to-refresh fired in the middle of a swipe. Or that the carousel moved and the page moved with it.
You open the handler and it is right there, the line that is supposed to stop exactly this:
document.addEventListener("touchmove", (e) => {
e.preventDefault()
drag(e)
})
It is being ignored. Not failing, not throwing, not running late. Ignored, by the specification, and the only notice anybody gets is a console warning on a device you do not have in front of you.
The rule, in the DOM Standard's own words
Every listener has a passive flag, and if you do not set one the browser picks for you. The DOM Standard calls this the default passive value, and it returns true when both of these hold:
type is one of "touchstart", "touchmove", "wheel", or "mousewheel".
eventTarget is a Window object, or is a node whose node document is eventTarget, or is a node whose node document's document element is eventTarget, or is a node whose node document's body element is eventTarget.
Read the second one slowly, because it is the whole problem. Those four targets are window, document, <html> and <body>. They are where everybody puts a delegated listener. The idiomatic place to attach a drag handler is precisely the set of places where your handler is silently made passive.
Attach the same callback to the carousel element instead and it is not passive, because that element is none of those four. Same code, same event, opposite behaviour, and nothing in the call tells you which you got.
What "passive" actually does to your call
It does not disable preventDefault(). It makes it do nothing, which is worse, because the method still exists and still returns.
The spec is blunt about the mechanism. Setting the canceled flag happens only "if event's cancelable attribute value is true and event's in passive listener flag is unset", and otherwise the instruction is to "do nothing". preventDefault() is defined as setting that flag. So inside a passive listener the call runs, succeeds as far as JavaScript is concerned, and changes nothing at all.
Then the sentence that matters most for anybody debugging this from a distance:
There are scenarios where invoking preventDefault() has no effect. User agents are encouraged to log the precise cause in a developer console, to aid debugging.
Encouraged. Not required. And even where a browser does log it, that line appears in the console of the phone in somebody else's hand, which is the one console you cannot read.
Why your own testing misses it
The desktop path through your drag code usually does not touch any of this. mousedown and mousemove are not on that list of four types, so a mouse drag calls preventDefault() and it works, every time, on your machine.
wheel is on the list, so a document-level wheel handler has the same problem, and that one you can reproduce. But the drag case is touch, and touch is the input you are least likely to be testing with.
So the bug exists only on the devices you use least, produces no exception, no failed network request and no error your tracker will ever receive, and the one diagnostic the spec mentions is optional and lands somewhere you cannot see.
The fix, which is one argument
// Passive by default. preventDefault() is ignored.
document.addEventListener("touchmove", onDrag)
// Explicitly not passive. preventDefault() works.
document.addEventListener("touchmove", onDrag, { passive: false })
// Also not passive: this target is not window, document, <html> or <body>.
carousel.addEventListener("touchmove", onDrag)
Say { passive: false } and you take the decision back. The cost is real and is the reason the default exists: the browser can no longer start scrolling before your handler has run, so a slow handler on that path becomes a janky scroll for everybody. That is a trade worth making deliberately on one element, and worth avoiding on document.
What reaches you
"It scrolls when I drag." Three words of a sentence, no error, no stack, nothing in your tracker, and a line of advice in a console that belongs to somebody else. The code you will be staring at contains the exact call that was supposed to prevent it, which is the most convincing wrong clue a bug can hand you.
Top comments (0)