Skip to content

Commit

Permalink
feat(engine): polyfill for non-composed click events (#568)
Browse files Browse the repository at this point in the history
* feat(engine): polyfill for non-composed click events

* test(engine): unit test

* feat(engine): enable the polyfill

* chore: review feedback

* fix: some browsers do not support MouseEvent

* fix: move click-event-composed polyfill before event-composed polyfill

Since the test to apply click-event-composed checks if a descriptor for the
composed property exists on the click event, the test for this polyfill needs
to run first.

* chore: review feedback

* refactor: unit tests
  • Loading branch information
ekashida authored Aug 14, 2018
1 parent 7e729c8 commit d15b77b
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/lwc-engine/src/framework/def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ export function getComponentDef(Ctor: ComponentConstructor): ComponentDef {

// Initialization Routines
import "../polyfills/proxy-concat/main";
import "../polyfills/click-event-composed/main"; // must come before event-composed
import "../polyfills/event-composed/main";
import "../polyfills/custom-event-composed/main";
import "../polyfills/focus-event-composed/main";
Expand Down
13 changes: 13 additions & 0 deletions packages/lwc-engine/src/polyfills/click-event-composed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# click-event-composed polyfill

This polyfill is needed to work around a Safari bug where click events are
not composed when generated as a result of invoking the click method.

Bug: https://bugs.webkit.org/show_bug.cgi?id=170211

This polyfill has a known limitation where click events passed to handlers
which are bound directly on the target are not patched. This is due to order
in which browsers invoke event handlers and the hook this polyfill uses to
patch the click event.

https://www.w3.org/TR/2003/NOTE-DOM-Level-3-Events-20031107/events.html#Events-listeners-registration
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { compileTemplate } from 'test-utils';
import { createElement, LightningElement } from '../../../framework/main';
import applyPolyfill from '../polyfill';

// TODO: https://github.com/salesforce/lwc/pull/568#discussion_r208827386
// While Jest creates a new window object between each test file evaluation, the
// jsdom code is not reevaluated. Which mean that the patched
// HTMLElement.prototype.click will remain patched for all the tests that happen
// to run in the same worker. This is a growing pain that we have today because
// it introduces an uncertainty in the way tests run. We really need to speak
// about to mitigate this issue in the future.
applyPolyfill();

describe('click-event-composed polyfill', () => {
const html = compileTemplate(`
<template>
<button onclick={handleClick}>click me</button>
</template>
`);

it('should patch click events for listeners bound to the host element', () => {
expect.assertions(1);

class Foo extends LightningElement {
renderedCallback() {
this.addEventListener('click', event => {
const isCustomClick = event instanceof CustomEvent;
if (!isCustomClick) {
return;
}
expect(event.composed).toBe(true);
});
this.template.querySelector('button').click();
}
render() {
return html;
}
handleClick(event: Event) {
const isCustomClick = event instanceof CustomEvent;
if (isCustomClick) {
return;
}
// Stop native click since we expect it to be composed in most
// browsers and substitute a non-composed version in its place.
event.stopPropagation();
const nonComposedClickEvent = new CustomEvent('click', { bubbles: true });
const button = event.target;
button.dispatchEvent(nonComposedClickEvent);
}
}

document.body.appendChild(createElement('x-foo', { is: Foo }));
});

it('should not patch click events for listeners bound to the target element (known limitation)', () => {
expect.assertions(1);

class Foo extends LightningElement {
renderedCallback() {
const button = this.template.querySelector('button');
button.addEventListener('click', event => {
const isCustomClick = event instanceof CustomEvent;
if (!isCustomClick) {
return;
}
expect(event.composed).toBe(false);
});
button.click();
}
render() {
return html;
}
handleClick(event: Event) {
const isCustomClick = event instanceof CustomEvent;
if (!isCustomClick) {
// Stop native click since we expect it to be composed in most
// browsers and substitute a non-composed version in its place.
event.stopPropagation();
const nonComposedClickEvent = new CustomEvent('click', { bubbles: true });
const button = event.target;
button.dispatchEvent(nonComposedClickEvent);
}
}
}

document.body.appendChild(createElement('x-foo', { is: Foo }));
});
});
20 changes: 20 additions & 0 deletions packages/lwc-engine/src/polyfills/click-event-composed/detect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const composedDescriptor = Object.getOwnPropertyDescriptor(Event.prototype, 'composed');

export default function detect(): boolean {
if (!composedDescriptor) {
// No need to apply this polyfill if this client completely lacks
// support for the composed property.
return false;
}

// Assigning a throwaway click event here to suppress a ts error when we
// pass clickEvent into the composed getter below. The error is:
// [ts] Variable 'clickEvent' is used before being assigned.
let clickEvent: Event = new Event('click');

const button = document.createElement('button');
button.addEventListener('click', event => clickEvent = event);
button.click();

return composedDescriptor.get!.call(clickEvent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import detect from './detect';
import apply from './polyfill';

if (detect()) {
apply();
}
25 changes: 25 additions & 0 deletions packages/lwc-engine/src/polyfills/click-event-composed/polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { addEventListener, removeEventListener } = Element.prototype;
const originalClickDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'click');

function handleClick(event) {
Object.defineProperty(event, 'composed', {
configurable: true,
enumerable: true,
get() {
return true;
}
});
}

export default function apply() {
HTMLElement.prototype.click = function() {
addEventListener.call(this, 'click', handleClick);
try {
originalClickDescriptor!.value!.call(this);
} catch (ex) {
throw ex;
} finally {
removeEventListener.call(this, 'click', handleClick);
}
};
}

0 comments on commit d15b77b

Please sign in to comment.