Modernizr: Add feature test for passive event listener support

Created on 19 Feb 2016  Â·  31Comments  Â·  Source: Modernizr/Modernizr

For passive event listeners, feature detection is important but non-trivial.

I don't have much experience with Modernizr but I can try to post a feature detect for this if desired.

Most helpful comment

Here's the test (and use) for event listener options and its passive option:

// assume the feature isn't supported
var supportsPassive = false;
// create options object with a getter to see if its passive property is accessed
var opts = Object.defineProperty && Object.defineProperty({}, 'passive', { get: function(){ supportsPassive = true }});
// create a throwaway element & event and (synchronously) test out our options
document.addEventListener('test', function() {}, opts);

// Use our detect's results. passive applied if supported, capture will be false either way.
elem.addEventListener('touchstart', fn, supportsPassive ? { passive: true } : false); 

The Object.defineProperty && guard only neccessary for IE8 and below. Likely not neccessary for most folks.


And a more terse version, assuming a modern runtime:

var supportsPassive = false;
document.createElement("div").addEventListener("test", _ => {}, { get passive() { supportsPassive = true }});

elem.addEventListener('touchstart', fn, supportsPassive ? { passive: true } : false); 

All 31 comments

Yes please! Happy to guide you through it, too

On Feb 19, 2016, at 12:28 PM, Rick Byers [email protected] wrote:

For passive event listeners, feature detection is important but non-trivial.

I don't have much experience with Modernizr but I can try to post a feature detect for this if desired.

—
Reply to this email directly or view it on GitHub.

Here's the test (and use) for event listener options and its passive option:

// assume the feature isn't supported
var supportsPassive = false;
// create options object with a getter to see if its passive property is accessed
var opts = Object.defineProperty && Object.defineProperty({}, 'passive', { get: function(){ supportsPassive = true }});
// create a throwaway element & event and (synchronously) test out our options
document.addEventListener('test', function() {}, opts);

// Use our detect's results. passive applied if supported, capture will be false either way.
elem.addEventListener('touchstart', fn, supportsPassive ? { passive: true } : false); 

The Object.defineProperty && guard only neccessary for IE8 and below. Likely not neccessary for most folks.


And a more terse version, assuming a modern runtime:

var supportsPassive = false;
document.createElement("div").addEventListener("test", _ => {}, { get passive() { supportsPassive = true }});

elem.addEventListener('touchstart', fn, supportsPassive ? { passive: true } : false); 

The terse option seems better we can just wrap in a try catch for older browsers.

Since the listener argument is nullable and addEventListener does nothing in that case, one can avoid the temporary element altogether, so:

var supportsPassive = false;
try {
  document.addEventListener("test", null, { get passive() { supportsPassive = true }});
} catch(e) {}

(If brevity is a plus, one can also omit document. as window is also an EventTarget.)

IE8 _has_ Object.defineProperty and it throws errors with non built-in objects. The && doesn't guard from throwing

Good call. Thx

Latest version is now over here: https://github.com/WICG/EventListenerOptions/pull/30#issuecomment-207420765

@ryanseddon You can't test syntax using try-catch, the whole thing will just not parse.

@mgol yep, we did try { eval() } catch() {} for ES2015 syntax feature tests but this is different to those.

Should I do something to get this listed in https://modernizr.com/docs?

it'll be listed automatically once I cut a new release, which will probably be tonight

Great, thanks!

​thank YOU

Any word on getting this into a release?

From a quick look, I didn't find a mention of passive event listener in https://modernizr.com/docs
Is the release coming soon?

Thanks in advance!

This isn't released yet. I dropped the

Modernizr.addTest('passiveeventlisteners', function() {
    var supportsPassiveOption = false;
    try {
      var opts = Object.defineProperty({}, 'passive', {
        get: function() {
          supportsPassiveOption = true;
        }
      });
      window.addEventListener('test', null, opts);
    } catch (e) {}
    return supportsPassiveOption;
  });

into our Modernizr build locally for now.

When will this be released? @patrickkettner @paulirish

How about cleaning up the event listener after adding finishing the test?

Modernizr.addTest('passiveeventlisteners', function() {
    var supportsPassiveOption = false;
    try {
      var opts = Object.defineProperty({}, 'passive', {
        get: function() {
          supportsPassiveOption = true;
        }
      });
      window.addEventListener('test', null, opts);
      window.removeEventListener('test', null, opts);
    } catch (e) {}
    return supportsPassiveOption;
  });

@Bamieh ... cleaning a null listener ? I hope browsers are smart enough to realize it's pointless regardless :-)

@WebReflection browsers will always disappoint you! no but seriously, nothing in the spec mentions testing the EventListener interface for null, hence the listener will stay registered. (1.3.1.)

Does it throw if it's triggered? What do specs say about dealing with null listener on dispatch? Anyway, i'm for cleaning up too, I just think in this case might be effectively unnecessary.

Okay after further investigation,
window.addEventListener('test', null, opts)
does not add an event listener to the window object in the first place, hence no need to remove anything.
Only tested on chrome though.
getEventListeners(window) // test is not listed in the object.

nothing in the spec mentions testing the EventListener interface for null, hence the listener will stay registered. (1.3.1.)

Spec is here. Step 2: "If callback is null, terminate these steps". I.e. this is defined to be a no-op and AFAIK all browsers implement it as such.

Looking at the spec here step 3
Let capture, passive, and once be the result of flattening more options.
will happen after the step 2 early return.

If I'm interpreting this correctly, this feature detection isn't spec compliant and likely most browsers aren't either given that this works in practice.

Perhaps the spec should be changed to explicitly allow for feature detection in this way?

hi @bgirard!
I _think_ you are misreading them, but it could very well be my own misunderstanding.

@domenic @RByers @annevk is @bgirard correct that the .passive property should not be being accessed by browsers without a callback also being defined (according to the spec)?

That's incorrect; it's accessed by the Web IDL bindings which convert the incoming object into a dictionary, and take place before any of the algorithm steps. I can appreciate that being pretty subtle though for those not familiar with Web IDL.

thanks for the quick response!

Thanks for clarifying. That means that this snippet works, unsurprisingly.

What about the spec being misleading in the way that step 2 and 3 is ordered? The spec is describing the flattening behavior that is occurring in Web IDL. Should we open a spec issue to re-order these steps to make it explicit that flattening should occur before the early return on a null callback?

The Web IDL conversion from object to dictionary and the "flattening" are orthogonal. Flattening operates on either a boolean or a dictionary; objects are never involved. There's no need to reorder the steps. It's not misleading to people who understand Web IDL, which again, I understand is not too many non-implementers. But you're looking at the algorithm for implementers, anyway.

Ohh I understand now. Thank you for clarifying!

without using Object.defineProperty:

//this variation will support more low-end/headless browsers.
self.is_support_passive = (function(){ "use strict";
  var is_support_passive = false
     ,opts               = {}
     ;

  function getter(){
    is_support_passive = true;
    return true;
  }

  function event_handler(ev){
    return true;
  }

  try{
  Object.prototype.__defineGetter__.call(opts, "passive", getter);
  window.addEventListener("touchend",    event_handler, opts);
  window.removeEventListener("touchend", event_handler, opts);
  }catch(err){}

  return is_support_passive;
}());

console.log(
  self.is_support_passive
);

If a browser is lacking Object.defineProperty,
it is most-likely too old to support passive events.

But since we're not checking the availability of Object.defineProperty,
using the example above is a (slightly) better-practice,
since it relays on an older feature, and limits try/catch to the very core.


Passing null will make a few passive-compatible versions of spider-monkey to fail silently
- and will give you a false-passive -is-unavailable status. Therefore it is a bad practice.

hey, if you assign a variable like this opts = {} you might as well know this is absolutely a redundant operation Object.prototype.__defineGetter__.call(opts, "passive", getter); 'cause the following one is exactly the same in 1/3rd of the code opts.__defineGetter__("passive", getter);

Moreover, this is not a "_most-likely_", this is a certain statement:

If a browser is lacking Object.defineProperty it is most-likely too old to support passive events

there is no browser on earth that would implement passive events without being compatible with ES5 so the code is just fine as it is

Was this page helpful?
0 / 5 - 0 ratings

Related issues

peterwilsoncc picture peterwilsoncc  Â·  9Comments

lovetrivedi picture lovetrivedi  Â·  4Comments

aaarichter picture aaarichter  Â·  6Comments

laukstein picture laukstein  Â·  4Comments

acschwartz picture acschwartz  Â·  6Comments