-
Notifications
You must be signed in to change notification settings - Fork 400
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(engine): polyfill for non-composed click events (#568)
* 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
Showing
6 changed files
with
153 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
packages/lwc-engine/src/polyfills/click-event-composed/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
88 changes: 88 additions & 0 deletions
88
packages/lwc-engine/src/polyfills/click-event-composed/__tests__/polyfill.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
20
packages/lwc-engine/src/polyfills/click-event-composed/detect.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
6 changes: 6 additions & 0 deletions
6
packages/lwc-engine/src/polyfills/click-event-composed/main.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
25
packages/lwc-engine/src/polyfills/click-event-composed/polyfill.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}; | ||
} |