Skip to content

Commit

Permalink
fix: event retargeting issue with nested component (#338)
Browse files Browse the repository at this point in the history
* fix: event retrageting issue with nested component

* test(engine): integration test for shadow root retargeting

* docs: add back removed comment
  • Loading branch information
pmdartus committed May 22, 2018
1 parent 8dd8392 commit a21121b
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 3 deletions.
38 changes: 38 additions & 0 deletions packages/lwc-engine/src/framework/__tests__/events.spec.ts
Expand Up @@ -746,6 +746,44 @@ describe('Shadow Root events', () => {
elm.clickChildDiv();
});
});

it('should retarget properly event listener attached on non-root components', () => {
expect.assertions(2);

class GrandChild extends Element {}

class Child extends Element {
connectedCallback() {
this.template.addEventListener('click', evt => {
expect(evt.target.tagName).toBe('X-GRAND-CHILD');
expect(evt.currentTarget.tagName).toBe('X-CHILD');
});
}

render() {
return $api => {
return [
$api.c('x-grand-child', GrandChild, {})
];
};
}
}

class Root extends Element {
render() {
return $api => {
return [
$api.c('x-child', Child, {})
];
};
}
}

const elm = createElement('x-root', { is: Root });
document.body.appendChild(elm);

HTMLElement.prototype.querySelector.call(elm, 'x-grand-child').click();
});
});

describe('Removing events from shadowroot', () => {
Expand Down
35 changes: 32 additions & 3 deletions packages/lwc-engine/src/framework/root.ts
Expand Up @@ -283,23 +283,52 @@ register({
// will kick in and return the cmp, which is not the intent.
return callback(pierce(value));
case 'target':
// For event listener attached:
// * on the component => `target` must always equal to `currentTarget`
// * on the root => `target` is retargeted
// * on an element inside template => `target` is retargeted
// * on the root component of the component tree via addEventListener
// => don't do any retargeting
//
// Slotted events should not be handled, since those are part of the light DOM.

const { currentTarget } = event;

// Executing event listener on component, target is always currentTarget
if (componentEventListenerType === EventListenerContext.COMPONENT_LISTENER) {
return callback(pierce(currentTarget));
}

// Event is coming from an slotted element
// Handle events is coming from an slotted elements.
// TODO: Add more information why we need to handle the light DOM events here.
if (isChildNode(getRootNode.call(value, GET_ROOT_NODE_CONFIG_FALSE), currentTarget as Element)) {
return;
}
// target is owned by the VM
const vm = currentTarget ? getElementOwnerVM(currentTarget as Element) : undefined;

let vm: VM | undefined;
if (componentEventListenerType === EventListenerContext.ROOT_LISTENER) {
// If we are in an event listener attached on the shadow root, then we do not want to look
// for the currentTarget owner VM because the currentTarget owner VM would be the VM which
// rendered the component (parent component).
//
// Instead, we want to get the custom element's VM because that VM owns the shadow root itself.
vm = getCustomElementVM(currentTarget as HTMLElement);
} else if (!isUndefined(currentTarget)) {
// TODO: When does currentTarget can be undefined
vm = getElementOwnerVM(currentTarget as Element);
}

// Handle case when VM is not present for example when attaching an event listener
// on the root component of the component tree.
if (!isUndefined(vm)) {
let node = value;

// Let's climb up the node tree starting from the original event target
// up until finding the first node being rendered by the current VM.
while (!isNull(node) && vm.uid !== node[OwnerKey]) {
node = node.parentNode;
}

return callback(pierce(node));
}
}
Expand Down
@@ -0,0 +1,17 @@
const assert = require('assert');

describe('Event target in slot elements', () => {
const URL = 'http://localhost:4567/root-listener-event-target/';

before(() => {
browser.url(URL);
});

it('should receive event with correct target', function () {
const element = browser.element('x-grand-child');
element.click();

const verifyElement = browser.element('.event-target-correct');
assert.strictEqual(verifyElement.getText(), 'Event Target is correct element');
});
});
@@ -0,0 +1,3 @@
<template>
<x-child></x-child>
</template>
@@ -0,0 +1,5 @@
import { Element } from 'engine';

export default class RootListenerEventTarget extends Element {

}
@@ -0,0 +1,4 @@
<template>
<x-grand-child></x-grand-child>
<div class="event-target-correct" if:true={eventTargetIsCorrect}>Event Target is correct element</div>
</template>
@@ -0,0 +1,11 @@
import { Element, track } from 'engine';

export default class Child extends Element {
@track eventTargetIsCorrect = false;

connectedCallback() {
this.template.addEventListener('click', (evt) => {
this.eventTargetIsCorrect = evt.target.tagName === 'X-GRAND-CHILD';
});
}
}
@@ -0,0 +1,3 @@
<template>
Click me
</template>
@@ -0,0 +1,5 @@
import { Element } from 'engine';

export default class GrandChild extends Element {

}

0 comments on commit a21121b

Please sign in to comment.