From e269b5d040c7f6f7abe1ddbede059a90982fccc8 Mon Sep 17 00:00:00 2001 From: Mikhail Bashkirov Date: Thu, 25 Jul 2019 17:03:25 +0200 Subject: [PATCH] fix(button): click event fired twice in IE11 (fix #179) --- packages/button/src/LionButton.js | 18 +++++++--- packages/button/test/lion-button.test.js | 43 ++++++++++++------------ 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/button/src/LionButton.js b/packages/button/src/LionButton.js index c7be8621c..f55e12558 100644 --- a/packages/button/src/LionButton.js +++ b/packages/button/src/LionButton.js @@ -20,7 +20,7 @@ export class LionButton extends DisabledWithTabIndexMixin( ${this._renderAfter()} -
+
`; } @@ -128,6 +128,7 @@ export class LionButton extends DisabledWithTabIndexMixin( constructor() { super(); this.role = 'button'; + this.__setupDelegationInConstructor(); } connectedCallback() { @@ -143,16 +144,19 @@ export class LionButton extends DisabledWithTabIndexMixin( _redispatchClickEvent(oldEvent) { // replacing `MouseEvent` with `oldEvent.constructor` breaks IE const newEvent = new MouseEvent(oldEvent.type, oldEvent); + newEvent.__isRedispatchedOnNativeButton = true; this.__enforceHostEventTarget(newEvent); this.$$slot('_button').dispatchEvent(newEvent); } /** - * Prevent click on the fake element and cause click on the native button. + * Prevent normal click and redispatch click on the native button unless already redispatched. */ __clickDelegationHandler(e) { - e.stopPropagation(); - this._redispatchClickEvent(e); + if (!e.__isRedispatchedOnNativeButton) { + e.stopImmediatePropagation(); + this._redispatchClickEvent(e); + } } __enforceHostEventTarget(event) { @@ -165,6 +169,12 @@ export class LionButton extends DisabledWithTabIndexMixin( } } + __setupDelegationInConstructor() { + // do not move to connectedCallback, otherwise IE11 breaks + // more info: https://github.com/ing-bank/lion/issues/179#issuecomment-511763835 + this.addEventListener('click', this.__clickDelegationHandler, true); + } + __setupDelegation() { this.addEventListener('keydown', this.__keydownDelegationHandler); this.addEventListener('keyup', this.__keyupDelegationHandler); diff --git a/packages/button/test/lion-button.test.js b/packages/button/test/lion-button.test.js index 1ad5334b1..1d94aaa46 100644 --- a/packages/button/test/lion-button.test.js +++ b/packages/button/test/lion-button.test.js @@ -9,10 +9,11 @@ import { import '../lion-button.js'; function getTopElement(el) { - const { left, top } = el.getBoundingClientRect(); + const { left, top, width, height } = el.getBoundingClientRect(); // to support elementFromPoint() in polyfilled browsers we have to use document - const crossBrowserRoot = el.shadowRoot.elementFromPoint ? el.shadowRoot : document; - return crossBrowserRoot.elementFromPoint(left, top); + const crossBrowserRoot = + el.shadowRoot && el.shadowRoot.elementFromPoint ? el.shadowRoot : document; + return crossBrowserRoot.elementFromPoint(left + width / 2, top + height / 2); } describe('lion-button', () => { @@ -151,7 +152,7 @@ describe('lion-button', () => { const clickSpy = sinon.spy(); const el = await fixture( html` - + foo `, ); @@ -164,27 +165,22 @@ describe('lion-button', () => { expect(clickSpy.callCount).to.equal(1); }); - describe('event after redispatching', async () => { - async function prepareClickEvent(el, host) { + describe('native button behavior', async () => { + async function prepareClickEvent(el) { setTimeout(() => { - if (host) { - // click on host like in native button - makeMouseEvent('click', { x: 11, y: 11 }, el); - } else { - // click on click-area which is then redispatched - makeMouseEvent('click', { x: 11, y: 11 }, getTopElement(el)); - } + makeMouseEvent('click', { x: 11, y: 11 }, getTopElement(el)); }); return oneEvent(el, 'click'); } - let hostEvent; - let redispatchedEvent; + let nativeButtonEvent; + let lionButtonEvent; before(async () => { - const el = await fixture(''); - hostEvent = await prepareClickEvent(el, true); - redispatchedEvent = await prepareClickEvent(el, false); + const nativeButtonEl = await fixture(''); + const lionButtonEl = await fixture('foo'); + nativeButtonEvent = await prepareClickEvent(nativeButtonEl); + lionButtonEvent = await prepareClickEvent(lionButtonEl); }); const sameProperties = [ @@ -194,14 +190,19 @@ describe('lion-button', () => { 'cancelable', 'clientX', 'clientY', - 'target', ]; sameProperties.forEach(property => { - it(`has same value of the property "${property}"`, async () => { - expect(redispatchedEvent[property]).to.equal(hostEvent[property]); + it(`has same value of the property "${property}" as in native button event`, () => { + expect(lionButtonEvent[property]).to.equal(nativeButtonEvent[property]); }); }); + + it('has host in the target property', async () => { + const el = await fixture('foo'); + const event = await prepareClickEvent(el); + expect(event.target).to.equal(el); + }); }); }); });