From 496f0a76a786beca7c66c84c018ae2316e0b6b8c Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Tue, 17 Dec 2013 15:10:21 -0500 Subject: [PATCH] Dispatch native events at all times If there are no listeners in a disconnected tree we add a temporary listener on the target which allows us to take over later in the process. Fixes #335 --- src/wrappers/events.js | 54 +++++++++++++++++++++++++++++++++++++++++- test/js/events.js | 24 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/wrappers/events.js b/src/wrappers/events.js index c162aba..ea967af 100644 --- a/src/wrappers/events.js +++ b/src/wrappers/events.js @@ -656,10 +656,62 @@ } }, dispatchEvent: function(event) { - dispatchEvent(event, this); + // We want to use the native dispatchEvent because it triggers the default + // actions (like checking a checkbox). However, if there are no listeners + // in the composed tree then there are no events that will trigger and + // listeners in the non composed tree that are part of the event path are + // not notified. + // + // If we find out that there are no listeners in the composed tree we add + // a temporary listener to the target which makes us get called back even + // in that case. + + var nativeEvent = unwrap(event); + var eventType = nativeEvent.type; + + // Allow dispatching the same event again. This is safe because if user + // code calls this during an existing dispatch of the same event the + // native dispatchEvent throws (that is required by the spec). + handledEventsTable.set(nativeEvent, false); + + // Force rendering since we prefer native dispatch and that works on the + // composed tree. + scope.renderAllPending(); + + var tempListener; + if (!hasListenerInAncestors(this, eventType)) { + tempListener = function() {}; + this.addEventListener(eventType, tempListener, true); + } + + try { + return unwrap(this).dispatchEvent_(nativeEvent); + } finally { + if (tempListener) + this.removeEventListener(eventType, tempListener, true); + } } }; + function hasListener(node, type) { + var listeners = listenersTable.get(node); + if (listeners) { + for (var i = 0; i < listeners.length; i++) { + if (!listeners[i].removed && listeners[i].type === type) + return true; + } + } + return false; + } + + function hasListenerInAncestors(target, type) { + for (var node = unwrap(target); node; node = node.parentNode) { + if (hasListener(wrap(node), type)) + return true; + } + return false; + } + if (OriginalEventTarget) registerWrapper(OriginalEventTarget, EventTarget); diff --git a/test/js/events.js b/test/js/events.js index 966eee5..01e1e15 100644 --- a/test/js/events.js +++ b/test/js/events.js @@ -1361,4 +1361,28 @@ test('retarget order (multiple shadow roots)', function() { assert.equal(data, 'b'); }); + test('dispatch should trigger default actions', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var checkbox = div.firstChild; + assert.isFalse(checkbox.checked); + checkbox.dispatchEvent(new MouseEvent('click')); + assert.isTrue(checkbox.checked); + }); + + test('dispatch should trigger default actions 2', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var checkbox = div.firstChild; + var sr = div.createShadowRoot(); + + assert.isFalse(checkbox.checked); + checkbox.dispatchEvent(new MouseEvent('click')); + assert.isTrue(checkbox.checked); + + div.offsetWidth; + checkbox.dispatchEvent(new MouseEvent('click')); + assert.isFalse(checkbox.checked); + }); + });