Merge pull request #157 from ing-bank/fix/button

[button] Fix redispatching of click event
This commit is contained in:
Mikhail Bashkirov 2019-07-12 16:32:33 +02:00 committed by GitHub
commit a2634b58bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 96 additions and 11 deletions

View file

@ -140,7 +140,6 @@ export class LionButton extends DelegateMixin(SlotMixin(LionLitElement)) {
this.disabled = false; this.disabled = false;
this.role = 'button'; this.role = 'button';
this.tabindex = 0; this.tabindex = 0;
this.__keydownDelegationHandler = this.__keydownDelegationHandler.bind(this);
} }
connectedCallback() { connectedCallback() {
@ -153,9 +152,29 @@ export class LionButton extends DelegateMixin(SlotMixin(LionLitElement)) {
this.__teardownDelegation(); this.__teardownDelegation();
} }
_redispatchClickEvent(oldEvent) {
// replacing `MouseEvent` with `oldEvent.constructor` breaks IE
const newEvent = new MouseEvent(oldEvent.type, oldEvent);
this.__enforceHostEventTarget(newEvent);
this.$$slot('_button').dispatchEvent(newEvent);
}
/**
* Prevent click on the fake element and cause click on the native button.
*/
__clickDelegationHandler(e) { __clickDelegationHandler(e) {
e.stopPropagation(); // prevent click on the fake element and cause click on the native button e.stopPropagation();
this.$$slot('_button').click(); this._redispatchClickEvent(e);
}
__enforceHostEventTarget(event) {
try {
// this is for IE11 (and works in others), because `Object.defineProperty` does not give any effect there
event.__defineGetter__('target', () => this); // eslint-disable-line no-restricted-properties
} catch (error) {
// in case `__defineGetter__` is removed from the platform
Object.defineProperty(event, 'target', { writable: false, value: this });
}
} }
__setupDelegation() { __setupDelegation() {
@ -181,7 +200,7 @@ export class LionButton extends DelegateMixin(SlotMixin(LionLitElement)) {
if (e.keyCode === 32 /* space */ || e.keyCode === 13 /* enter */) { if (e.keyCode === 32 /* space */ || e.keyCode === 13 /* enter */) {
e.preventDefault(); e.preventDefault();
this.shadowRoot.querySelector('.btn').removeAttribute('active'); this.shadowRoot.querySelector('.btn').removeAttribute('active');
this.$$slot('_button').click(); this.shadowRoot.querySelector('.click-area').click();
} }
} }

View file

@ -1,9 +1,20 @@
import { expect, fixture, html, aTimeout } from '@open-wc/testing'; import { expect, fixture, html, aTimeout, oneEvent } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
import { pressEnter, pressSpace } from '@polymer/iron-test-helpers/mock-interactions.js'; import {
makeMouseEvent,
pressEnter,
pressSpace,
} from '@polymer/iron-test-helpers/mock-interactions.js';
import '../lion-button.js'; import '../lion-button.js';
function getTopElement(el) {
const { left, top } = 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);
}
describe('lion-button', () => { describe('lion-button', () => {
it('behaves like native `button` in terms of a11y', async () => { it('behaves like native `button` in terms of a11y', async () => {
const el = await fixture(`<lion-button>foo</lion-button>`); const el = await fixture(`<lion-button>foo</lion-button>`);
@ -99,11 +110,7 @@ describe('lion-button', () => {
`); `);
const button = form.querySelector('lion-button'); const button = form.querySelector('lion-button');
const { left, top } = button.getBoundingClientRect(); getTopElement(button).click();
// to support elementFromPoint() in polyfilled browsers we have to use document
const crossBrowserRoot = button.shadowRoot.elementFromPoint ? button.shadowRoot : document;
const shadowClickAreaElement = crossBrowserRoot.elementFromPoint(left, top);
shadowClickAreaElement.click();
expect(formSubmitSpy.called).to.be.true; expect(formSubmitSpy.called).to.be.true;
}); });
@ -138,4 +145,63 @@ describe('lion-button', () => {
expect(formSubmitSpy.called).to.be.true; expect(formSubmitSpy.called).to.be.true;
}); });
}); });
describe('click event', () => {
it('is fired once', async () => {
const clickSpy = sinon.spy();
const el = await fixture(
html`
<lion-button @click="${clickSpy}"></lion-button>
`,
);
getTopElement(el).click();
// trying to wait for other possible redispatched events
await aTimeout();
await aTimeout();
expect(clickSpy.callCount).to.equal(1);
});
describe('event after redispatching', async () => {
async function prepareClickEvent(el, host) {
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));
}
});
return oneEvent(el, 'click');
}
let hostEvent;
let redispatchedEvent;
before(async () => {
const el = await fixture('<lion-button></lion-button>');
hostEvent = await prepareClickEvent(el, true);
redispatchedEvent = await prepareClickEvent(el, false);
});
const sameProperties = [
'constructor',
'composed',
'bubbles',
'cancelable',
'clientX',
'clientY',
'target',
];
sameProperties.forEach(property => {
it(`has same value of the property "${property}"`, async () => {
expect(redispatchedEvent[property]).to.equal(hostEvent[property]);
});
});
});
});
}); });