Skip to content

Commit

Permalink
Merge pull request #758 from michaelw85/755_fix_shadow_dom_event_bubb…
Browse files Browse the repository at this point in the history
…ling

fix(event-manager): shadow dom event bubbling
  • Loading branch information
EisenbergEffect authored Sep 5, 2019
2 parents 5673f9a + de5b880 commit ae58f99
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 85 deletions.
65 changes: 37 additions & 28 deletions src/event-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,49 +67,53 @@ class CapturedHandlerEntry {
}
}

function handleDelegatedEvent(event) {
event.propagationStopped = false;
let target = findOriginalEventTarget(event);
class DelegateHandlerEntry {
constructor(eventName, eventManager) {
this.eventName = eventName;
this.count = 0;
this.eventManager = eventManager;
}

while (target && !event.propagationStopped) {
if (target.delegatedCallbacks) {
let callback = target.delegatedCallbacks[event.type];
if (callback) {
if (event.stopPropagation !== stopPropagation) {
event.standardStopPropagation = event.stopPropagation;
event.stopPropagation = stopPropagation;
}
if ('handleEvent' in callback) {
callback.handleEvent(event);
} else {
callback(event);
handleEvent(event) {
event.propagationStopped = false;
let target = findOriginalEventTarget(event);

while (target && !event.propagationStopped) {
if (target.delegatedCallbacks) {
let callback = target.delegatedCallbacks[event.type];
if (callback) {
if (event.stopPropagation !== stopPropagation) {
event.standardStopPropagation = event.stopPropagation;
event.stopPropagation = stopPropagation;
}
if ('handleEvent' in callback) {
callback.handleEvent(event);
} else {
callback(event);
}
}
}
}

target = target.parentNode;
}
}
const parent = target.parentNode;
const shouldEscapeShadowRoot = this.eventManager.escapeShadowRoot && parent instanceof ShadowRoot;

class DelegateHandlerEntry {
constructor(eventName) {
this.eventName = eventName;
this.count = 0;
target = shouldEscapeShadowRoot ? parent.host : parent;
}
}

increment() {
this.count++;

if (this.count === 1) {
DOM.addEventListener(this.eventName, handleDelegatedEvent, false);
DOM.addEventListener(this.eventName, this, false);
}
}

decrement() {
if (this.count === 0) {
emLogger.warn('The same EventListener was disposed multiple times.');
} else if (--this.count === 0) {
DOM.removeEventListener(this.eventName, handleDelegatedEvent, false);
DOM.removeEventListener(this.eventName, this, false);
}
}
}
Expand Down Expand Up @@ -163,6 +167,10 @@ class DefaultEventStrategy {
delegatedHandlers = {};
capturedHandlers = {};

constructor(eventManager) {
this.eventManager = eventManager;
}

/**
* @param {Element} target
* @param {string} targetEvent
Expand All @@ -177,7 +185,7 @@ class DefaultEventStrategy {

if (strategy === delegationStrategy.bubbling) {
delegatedHandlers = this.delegatedHandlers;
handlerEntry = delegatedHandlers[targetEvent] || (delegatedHandlers[targetEvent] = new DelegateHandlerEntry(targetEvent));
handlerEntry = delegatedHandlers[targetEvent] || (delegatedHandlers[targetEvent] = new DelegateHandlerEntry(targetEvent, this.eventManager));
let delegatedCallbacks = target.delegatedCallbacks || (target.delegatedCallbacks = {});
if (!delegatedCallbacks[targetEvent]) {
handlerEntry.increment();
Expand Down Expand Up @@ -235,9 +243,10 @@ export const delegationStrategy = {
};

export class EventManager {
constructor() {
constructor(escapeShadowRoot = false) {
this.elementHandlerLookup = {};
this.eventStrategyLookup = {};
this.escapeShadowRoot = escapeShadowRoot;

this.registerElementConfig({
tagName: 'input',
Expand Down Expand Up @@ -277,7 +286,7 @@ export class EventManager {
}
});

this.defaultEventStrategy = new DefaultEventStrategy();
this.defaultEventStrategy = new DefaultEventStrategy(this);
}

registerElementConfig(config) {
Expand Down
156 changes: 99 additions & 57 deletions test/event-manager.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import * as LogManager from 'aurelia-logging';

describe('EventManager', () => {
describe('getElementHandler', () => {
var em;
let em;
beforeAll(() => em = new EventManager());

it('handles input', () => {
var element = DOM.createElement('input');
let element = DOM.createElement('input');

expect(em.getElementHandler(element, 'value')).not.toBeNull();
expect(em.getElementHandler(element, 'checked')).not.toBeNull();
Expand All @@ -18,41 +18,41 @@ describe('EventManager', () => {
});

it('handles textarea', () => {
var element = DOM.createElement('textarea');
let element = DOM.createElement('textarea');

expect(em.getElementHandler(element, 'value')).not.toBeNull();
expect(em.getElementHandler(element, 'id')).toBeNull();
});

it('handles select', () => {
var element = DOM.createElement('select');
let element = DOM.createElement('select');

expect(em.getElementHandler(element, 'value')).not.toBeNull();
expect(em.getElementHandler(element, 'id')).toBeNull();
});

it('handles textContent and innerHTML properties', () => {
var element = DOM.createElement('div');
let element = DOM.createElement('div');

expect(em.getElementHandler(element, 'textContent')).not.toBeNull();
expect(em.getElementHandler(element, 'innerHTML')).not.toBeNull();
expect(em.getElementHandler(element, 'id')).toBeNull();
});

it('handles scrollTop and scrollLeft properties', () => {
var element = DOM.createElement('div');
let element = DOM.createElement('div');

expect(em.getElementHandler(element, 'scrollTop')).not.toBeNull();
expect(em.getElementHandler(element, 'scrollLeft')).not.toBeNull();
expect(em.getElementHandler(element, 'id')).toBeNull();
});

it('can subscribe', () => {
var element = DOM.createElement('input'),
handler = em.getElementHandler(element, 'value'),
dispose,
callback = jasmine.createSpy('callback'),
inputEvent = DOM.createCustomEvent('input');
let element = DOM.createElement('input'),
handler = em.getElementHandler(element, 'value'),
dispose,
callback = jasmine.createSpy('callback'),
inputEvent = DOM.createCustomEvent('input');
element.value = 'foo';
expect(handler).toBeDefined();
expect(handler.subscribe).toBeDefined();
Expand All @@ -72,29 +72,40 @@ describe('EventManager', () => {
});

describe('addEventListener', () => {
const em = new EventManager();
const one = document.createElement('div');
const two = document.createElement('div');
const three = document.createElement('div');

const oneClick = jasmine.createSpy('one-click');
const threeClick = jasmine.createSpy('three-click');
const oneDelegate = jasmine.createSpy('one-delegate');
const threeDelegate = jasmine.createSpy('three-delegate');
let em, one, two, three, shadowHost, shadowRoot, shadowButton, oneClick, threeClick, oneDelegate, threeDelegate, delegationEntryHandlers;

beforeEach(() => {
em = new EventManager();
one = document.createElement('div');
two = document.createElement('div');
three = document.createElement('div');
shadowHost = document.createElement('div');
shadowButton = document.createElement('button');

oneClick = jasmine.createSpy('one-click');
threeClick = jasmine.createSpy('three-click');
oneDelegate = jasmine.createSpy('one-delegate');
threeDelegate = jasmine.createSpy('three-delegate');

document.body.appendChild(one);
one.appendChild(two);
two.appendChild(three);
one.appendChild(shadowHost);

em.addEventListener(one, 'click', oneClick, delegationStrategy.none);
em.addEventListener(three, 'click', threeClick, delegationStrategy.none);
em.addEventListener(one, 'delegate', oneDelegate, delegationStrategy.bubbling);
em.addEventListener(three, 'delegate', threeDelegate, delegationStrategy.bubbling);
shadowRoot = shadowHost.attachShadow({ mode: 'open' });
shadowRoot.appendChild(shadowButton);

delegationEntryHandlers = [
em.addEventListener(one, 'click', oneClick, delegationStrategy.none, true),
em.addEventListener(three, 'click', threeClick, delegationStrategy.none, true),
em.addEventListener(one, 'delegate', oneDelegate, delegationStrategy.bubbling, true),
em.addEventListener(three, 'delegate', threeDelegate, delegationStrategy.bubbling, true)
];
});

afterEach(() => {
one.parentNode.removeChild(one);
delegationEntryHandlers.forEach(delegationEntryHandler => delegationEntryHandler.dispose());
one.remove();
});

it('bubbles properly when not delegated', () => {
Expand All @@ -116,8 +127,21 @@ describe('EventManager', () => {
one.dispatchEvent(oneClickEvent);
expect(threeClick).not.toHaveBeenCalledWith(threeClickEvent);
expect(oneClick).toHaveBeenCalledWith(oneClickEvent);
oneClick.calls.reset();
threeClick.calls.reset();
});


it('bubbles properly out of shadow dom when not delegated with composed flag', () => {
em.escapeShadowRoot = true;
const shadowButtonClickEvent = DOM.createCustomEvent('click', { bubbles: true, composed: true });
shadowButton.dispatchEvent(shadowButtonClickEvent);
expect(oneClick).toHaveBeenCalledWith(shadowButtonClickEvent);
});

it('should not bubble out of shadow dom when not delegated without composed flag', () => {
em.escapeShadowRoot = true;
const shadowButtonClickEvent = DOM.createCustomEvent('click', { bubbles: true });
shadowButton.dispatchEvent(shadowButtonClickEvent);
expect(oneClick).not.toHaveBeenCalledWith(shadowButtonClickEvent);
});

it('bubbles properly when delegated', () => {
Expand All @@ -139,8 +163,26 @@ describe('EventManager', () => {
one.dispatchEvent(oneDelegateEvent);
expect(threeDelegate).not.toHaveBeenCalledWith(threeDelegateEvent);
expect(oneDelegate).toHaveBeenCalledWith(oneDelegateEvent);
oneDelegate.calls.reset();
threeDelegate.calls.reset();
});

it('should not bubble out of shadow dom when escapeShadowRoot is not explicitly set', () => {
const shadowButtonDelegateEvent = DOM.createCustomEvent('delegate', { bubbles: true, composed: true });
shadowButton.dispatchEvent(shadowButtonDelegateEvent);
expect(oneDelegate).not.toHaveBeenCalled();
});

it('bubbles properly out of shadow dom when delegated with composed flag', () => {
em.escapeShadowRoot = true;
const shadowButtonDelegateEvent = DOM.createCustomEvent('delegate', { bubbles: true, composed: true });
shadowButton.dispatchEvent(shadowButtonDelegateEvent);
expect(oneDelegate).toHaveBeenCalledWith(shadowButtonDelegateEvent);
});

it('should not bubble out of shadow dom when delegated without composed flag', () => {
em.escapeShadowRoot = true;
const shadowButtonDelegateEvent = DOM.createCustomEvent('delegate', { bubbles: true });
shadowButton.dispatchEvent(shadowButtonDelegateEvent);
expect(oneDelegate).not.toHaveBeenCalledWith(shadowButtonDelegateEvent);
});

it('stops bubbling when asked', () => {
Expand Down Expand Up @@ -184,7 +226,7 @@ describe('EventManager', () => {
let originalWarn;
beforeEach(() => {
originalWarn = LogManager.Logger.prototype.warn;
spyOn(LogManager.Logger.prototype, 'warn')
spyOn(LogManager.Logger.prototype, 'warn');
});
afterEach(() => {
LogManager.Logger.prototype.warn = originalWarn;
Expand All @@ -207,7 +249,7 @@ describe('EventManager', () => {
em.addEventListener(one, 'delegate', oneDelegate, delegationStrategy.bubbling, true),
em.addEventListener(three, 'delegate', threeDelegate, delegationStrategy.bubbling, true),
em.addEventListener(one, 'delegate', oneCapture, delegationStrategy.capturing, true),
em.addEventListener(three, 'delegate', threeCapture, delegationStrategy.capturing, true),
em.addEventListener(three, 'delegate', threeCapture, delegationStrategy.capturing, true)
];

let threeClickEvent = DOM.createCustomEvent('click', { bubbles: true });
Expand All @@ -217,24 +259,24 @@ describe('EventManager', () => {
oneClick.calls.reset();
threeClick.calls.reset();

let delegateBubblingCount = em.defaultEventStrategy.delegatedHandlers['delegate'].count;
let delegateBubblingCount = em.defaultEventStrategy.delegatedHandlers.delegate.count;

handlers.push(
em.addEventListener(one, 'delegate', oneDelegate, delegationStrategy.bubbling, true),
em.addEventListener(three, 'delegate', threeDelegate, delegationStrategy.bubbling, true)
);
let delegateBubblingCountAfterDoubleSubscription = em.defaultEventStrategy.delegatedHandlers['delegate'].count;
expect(delegateBubblingCountAfterDoubleSubscription).toEqual(delegateBubblingCount,"allows double subscription for bubbling");
let delegateBubblingCountAfterDoubleSubscription = em.defaultEventStrategy.delegatedHandlers.delegate.count;
expect(delegateBubblingCountAfterDoubleSubscription).toEqual(delegateBubblingCount, 'allows double subscription for bubbling');
expect(LogManager.Logger.prototype.warn).toHaveBeenCalled();
let delegateCaptureCount = em.defaultEventStrategy.delegatedHandlers['delegate'].count;
let delegateCaptureCount = em.defaultEventStrategy.delegatedHandlers.delegate.count;

handlers.push(
em.addEventListener(one, 'delegate', oneCapture, delegationStrategy.capturing, true),
em.addEventListener(three, 'delegate', threeCapture, delegationStrategy.capturing, true),
)
expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(2)
let delegateCaptureCountAfterDoubleSubscription = em.defaultEventStrategy.capturedHandlers['delegate'].count;
expect(delegateCaptureCountAfterDoubleSubscription).toEqual(delegateCaptureCount,"allows double subscription for capture");
);
expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(2);
let delegateCaptureCountAfterDoubleSubscription = em.defaultEventStrategy.capturedHandlers.delegate.count;
expect(delegateCaptureCountAfterDoubleSubscription).toEqual(delegateCaptureCount, 'allows double subscription for capture');

handlers[0].dispose();
handlers[1].dispose();
Expand All @@ -257,26 +299,26 @@ describe('EventManager', () => {
threeCapture.calls.reset();
oneCapture.calls.reset();

handlers[2].dispose();
let delegateBubblingCountBeforeDisposingDisposed = em.defaultEventStrategy.delegatedHandlers['delegate'].count;
handlers[2].dispose();
let delegateBubblingCountAfterDisposingDisposed = em.defaultEventStrategy.delegatedHandlers['delegate'].count;
expect(delegateBubblingCountAfterDisposingDisposed).toEqual(delegateBubblingCountBeforeDisposingDisposed,"allows double disposing for bubbling");
expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(3)
handlers[2].dispose();
let delegateBubblingCountBeforeDisposingDisposed = em.defaultEventStrategy.delegatedHandlers.delegate.count;
handlers[2].dispose();
let delegateBubblingCountAfterDisposingDisposed = em.defaultEventStrategy.delegatedHandlers.delegate.count;
expect(delegateBubblingCountAfterDisposingDisposed).toEqual(delegateBubblingCountBeforeDisposingDisposed, 'allows double disposing for bubbling');
expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(3);
handlers[3].dispose();
handlers[4].dispose();
let delegateCaptureCountBeforeDisposingDisposed = em.defaultEventStrategy.capturedHandlers['delegate'].count;
handlers[4].dispose();
let delegateCaptureCountAfterDisposingDisposed = em.defaultEventStrategy.capturedHandlers['delegate'].count;
expect(delegateCaptureCountAfterDisposingDisposed).toEqual(delegateCaptureCountBeforeDisposingDisposed,"allows double disposing for bubbling");
expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(4)

handlers[4].dispose();
let delegateCaptureCountBeforeDisposingDisposed = em.defaultEventStrategy.capturedHandlers.delegate.count;
handlers[4].dispose();
let delegateCaptureCountAfterDisposingDisposed = em.defaultEventStrategy.capturedHandlers.delegate.count;
expect(delegateCaptureCountAfterDisposingDisposed).toEqual(delegateCaptureCountBeforeDisposingDisposed, 'allows double disposing for bubbling');
expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(4);
handlers[5].dispose();

em.defaultEventStrategy.capturedHandlers['delegate'].decrement();
em.defaultEventStrategy.capturedHandlers.delegate.decrement();
expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(5);
em.defaultEventStrategy.delegatedHandlers['delegate'].decrement();
expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(6)
em.defaultEventStrategy.delegatedHandlers.delegate.decrement();
expect(LogManager.Logger.prototype.warn).toHaveBeenCalledTimes(6);

threeDelegateEvent = DOM.createCustomEvent('delegate', { bubbles: true });
three.dispatchEvent(threeDelegateEvent);
Expand Down

0 comments on commit ae58f99

Please sign in to comment.