diff --git a/lib/common/events.ts b/lib/common/events.ts index b2e7074f6..4d404b427 100644 --- a/lib/common/events.ts +++ b/lib/common/events.ts @@ -18,6 +18,23 @@ interface EventTaskData extends TaskData { readonly useG?: boolean; } +let passiveSupported = false; + +if (typeof window !== 'undefined') { + try { + const options = Object.defineProperty({}, 'passive', { + get: function() { + passiveSupported = true; + } + }); + + window.addEventListener('test', options, options); + window.removeEventListener('test', options, options); + } catch (err) { + passiveSupported = false; + } +} + // an identifier to tell ZoneTask do not create a new invoke closure const OPTIMIZED_ZONE_EVENT_TASK_DATA: EventTaskData = { useG: true @@ -50,6 +67,8 @@ export interface PatchEventTargetOptions { rt?: boolean; // event compare handler diff?: (task: any, delegate: any) => boolean; + // support passive or not + supportPassive?: boolean; } export function patchEventTarget( @@ -212,12 +231,25 @@ export function patchEventTarget( proto[patchOptions.prepend]; } - const customScheduleGlobal = function() { + function checkIsPassive(task: Task) { + if (!passiveSupported && typeof taskData.options !== 'boolean' && + typeof taskData.options !== 'undefined' && taskData.options !== null) { + // options is a non-null non-undefined object + // passive is not supported + // don't pass options as object + // just pass capture as a boolean + (task as any).options = !!taskData.options.capture; + taskData.options = (task as any).options; + } + } + + const customScheduleGlobal = function(task: Task) { // if there is already a task for the eventName + capture, // just return, because we use the shared globalZoneAwareCallback here. if (taskData.isExisting) { return; } + checkIsPassive(task); return nativeAddEventListener.call( taskData.target, taskData.eventName, taskData.capture ? globalZoneAwareCaptureCallback : globalZoneAwareCallback, @@ -265,6 +297,7 @@ export function patchEventTarget( }; const customScheduleNonGlobal = function(task: Task) { + checkIsPassive(task); return nativeAddEventListener.call( taskData.target, taskData.eventName, task.invoke, taskData.options); }; @@ -421,7 +454,11 @@ export function patchEventTarget( if (once) { options.once = true; } - task.options = options; + if (!(!passiveSupported && typeof task.options === 'boolean')) { + // if not support passive, and we pass an option object + // to addEventListener, we should save the options to task + task.options = options; + } task.target = target; task.capture = capture; task.eventName = eventName; diff --git a/test/browser/browser.spec.ts b/test/browser/browser.spec.ts index 6afa1cfe4..0ca232fb3 100644 --- a/test/browser/browser.spec.ts +++ b/test/browser/browser.spec.ts @@ -47,8 +47,8 @@ try { supportsPassive = true; } }); - window.addEventListener('test', null as any, opts); - window.removeEventListener('test', null as any, opts); + window.addEventListener('test', opts as any, opts); + window.removeEventListener('test', opts as any, opts); } catch (e) { } @@ -75,6 +75,14 @@ function ieOrEdge() { (ieOrEdge as any).message = 'IE/Edge Test'; +class TestEventListener { + logs: string[] = []; + addEventListener(eventName: string, listener: any, options: any) { + this.logs.push(options); + } + removeEventListener(eventName: string, listener: any, options: any) {} +} + describe('Zone', function() { const rootZone = Zone.current; (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; @@ -996,6 +1004,46 @@ describe('Zone', function() { expect(logs).toEqual(['click']); })); + it('should change options to boolean if not support passive', () => { + patchEventTarget(window, [TestEventListener.prototype]); + const testEventListener = new TestEventListener(); + + const listener = function() {}; + testEventListener.addEventListener('test', listener, {passive: true}); + testEventListener.addEventListener('test1', listener, {once: true}); + testEventListener.addEventListener('test2', listener, {capture: true}); + testEventListener.addEventListener('test3', listener, {passive: false}); + testEventListener.addEventListener('test4', listener, {once: false}); + testEventListener.addEventListener('test5', listener, {capture: false}); + if (!supportsPassive) { + expect(testEventListener.logs).toEqual([false, false, true, false, false, false]); + } else { + expect(testEventListener.logs).toEqual([ + {passive: true}, {once: true}, {capture: true}, {passive: false}, {once: false}, + {capture: false} + ]); + } + }); + + it('should change options to boolean if not support passive on HTMLElement', () => { + const logs: string[] = []; + const listener = (e: Event) => { + logs.push('clicked'); + }; + + (button as any).addEventListener('click', listener, {once: true}); + button.dispatchEvent(clickEvent); + expect(logs).toEqual(['clicked']); + button.dispatchEvent(clickEvent); + if (supportsPassive) { + expect(logs).toEqual(['clicked']); + } else { + expect(logs).toEqual(['clicked', 'clicked']); + } + + button.removeEventListener('click', listener); + }); + it('should support addEventListener with AddEventListenerOptions passive setting', ifEnvSupports(supportEventListenerOptions, function() { const hookSpy = jasmine.createSpy('hook');