Unable to preventDefault inside passive event listener invocation.

We have been working on making our Laravel / Vue / JS app more efficient and smoother. Based on analysis and console warning we had been making tweaks. We had just added code to make all events passive by default based on tips from the Chrome Inspector.

Passive Events

The reason to make the event event is to make scrolling smoother and everything quicker. Let me quote from a Google Developer blog post here:

If you call preventDefault() in the touchstart or first touchmove events then you will prevent scrolling. The problem is that most often listeners will not call preventDefault(), but the browser needs to wait for the event to finish to be sure of that. Developer-defined “passive event listeners” solve this. When you add a touch event with a {passive: true} object as the third parameter in your event handler then you are telling the browser that the touchstart listener will not call preventDefault() and the browser can safely perform the scroll without blocking on the listener.

Chrome Blog Post url

And this warning we got related to a TinyMCE implementation.

Added non-passive event listener to a scroll-blocking

So as said we were told we were using a scroll blocking event and that this would slow dow the app’s experience. The specific error we had before was the following:

[Violation] Added non-passive event listener to a scroll-blocking 'touchstart' event.
Consider marking event handler as 'passive' to make the page more responsive.
See https://www.chromestatus.com/feature/5745543795965952

It was pointing at an issue with TinyMCE and in our case TinyMCE used in a drag and drop toolbar on an html canvas.

Passive Event Listeners

We made this event and all events passive by default using the following code:

(function () {
let func = EventTarget.prototype.addEventListener;
let supportsPassive = false
try {
let opts = Object.defineProperty({}, 'passive', {
get: function() {
supportsPassive = true;
}
})

document.addEventListener("testPassive", null, opts);
document.removeEventListener('testPassive', null, opts);
} catch(e) {}

EventTarget.prototype.addEventListener = function (type, fn, capture) {
this.func = func;
capture = capture instanceof Object ? capture : {};
capture.passive = supportsPassive;
this.func(type, fn, capture);
};
}());

As you can see it also has a check to see if passive is supported and also sends out an error report when there is an issue. Adding passive: true to an EventListener makes it a no longer blocking event which is good for performance.

Unable to preventDefault

But as soon as we started using this our elements could no longer be dropped on the canvas and we got this warning from the console

Unable to preventDefault inside passive event listener invocation.

We were using preventDefault to allow items to be dropped on the canvas. This function entails the following:

The Event interface’s preventDefault() method tells the user agent that if the event does not get explicitly handled, its default action should not be taken as it normally would be. The event continues to propagate as usual, unless one of its event listeners calls stopPropagation() or stopImmediatePropagation(), either of which terminates propagation at once.

The reason we added this as dropping an element into another is not possible by default (w3 schools basics on this). Specific code block with preventDefault in question was this basic one:

blockEvent: function(event) {
event.preventDefault();
},

It was attached to the canvas div block using Vue v-on directives @dragenter="blockEvent" @dragover="blockEvent"more on html drag and drop api here. But now that we added the default passive is true to events the preventDefault function no longer worked disallowing us to drop elements on the canvas. So what to do?

Solution

As possible solutions the Google Developer blog post mentioned the following:

We’ve found that a large majority of impacted pages are fixed relatively easily by applying the touch-action CSS property whenever possible. If you wish to prevent all browser scrolling and zooming within an element apply touch-action: none to it.

In more complex cases it may be necessary to also rely on one of the following:

  • If your touchstart listener calls preventDefault(), ensure preventDefault() is also called from associated touchend listeners to continue suppressing the generation of click events and other default tap behavior.
  • Last (and discouraged) pass {passive: false} to addEventListener() to override the default behavior. Be aware you will have to feature detect if the User Agent supports EventListenerOptions.

We decided to implement the following …

 

Jasper Frumau

Jasper has been working with web frameworks and applications such as Laravel, Magento and his favorite CMS WordPress including Roots Trellis and Sage for more than a decade. He helps customers with web design and online marketing. Services provided are web design, ecommerce, SEO, content marketing. When Jasper is not coding, marketing a website, reading about the web or dreaming the internet of things he plays with his son, travels or run a few blocks.