diff --git a/packages/button/src/LionButton.js b/packages/button/src/LionButton.js
index c7be8621c..46526e5f3 100644
--- a/packages/button/src/LionButton.js
+++ b/packages/button/src/LionButton.js
@@ -10,6 +10,10 @@ export class LionButton extends DisabledWithTabIndexMixin(
type: String,
reflect: true,
},
+ active: {
+ type: Boolean,
+ reflect: true,
+ },
};
}
@@ -20,7 +24,7 @@ export class LionButton extends DisabledWithTabIndexMixin(
${this._renderAfter()}
-
+
`;
}
@@ -83,8 +87,8 @@ export class LionButton extends DisabledWithTabIndexMixin(
background: #f4f6f7;
}
- :host(:active) .btn,
- .btn[active] {
+ :host(:active) .btn, /* keep native :active to render quickly where possible */
+ :host([active]) .btn /* use custom [active] to fix IE11 */ {
/* if you extend, please overwrite */
background: gray;
}
@@ -128,31 +132,36 @@ export class LionButton extends DisabledWithTabIndexMixin(
constructor() {
super();
this.role = 'button';
+ this.active = false;
+ this.__setupDelegationInConstructor();
}
connectedCallback() {
super.connectedCallback();
- this.__setupDelegation();
+ this.__setupEvents();
}
disconnectedCallback() {
super.disconnectedCallback();
- this.__teardownDelegation();
+ this.__teardownEvents();
}
_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,30 +174,56 @@ export class LionButton extends DisabledWithTabIndexMixin(
}
}
- __setupDelegation() {
- this.addEventListener('keydown', this.__keydownDelegationHandler);
- this.addEventListener('keyup', this.__keyupDelegationHandler);
+ __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);
}
- __teardownDelegation() {
- this.removeEventListener('keydown', this.__keydownDelegationHandler);
- this.removeEventListener('keyup', this.__keyupDelegationHandler);
+ __setupEvents() {
+ this.addEventListener('mousedown', this.__mousedownHandler);
+ this.addEventListener('keydown', this.__keydownHandler);
+ this.addEventListener('keyup', this.__keyupHandler);
}
- __keydownDelegationHandler(e) {
- if (e.keyCode === 32 /* space */ || e.keyCode === 13 /* enter */) {
- e.preventDefault();
- this.shadowRoot.querySelector('.btn').setAttribute('active', '');
+ __teardownEvents() {
+ this.removeEventListener('mousedown', this.__mousedownHandler);
+ this.removeEventListener('keydown', this.__keydownHandler);
+ this.removeEventListener('keyup', this.__keyupHandler);
+ }
+
+ __mousedownHandler() {
+ this.active = true;
+ const mouseupHandler = () => {
+ this.active = false;
+ document.removeEventListener('mouseup', mouseupHandler);
+ };
+ document.addEventListener('mouseup', mouseupHandler);
+ }
+
+ __keydownHandler(e) {
+ if (this.active || !this.__isKeyboardClickEvent(e)) {
+ return;
}
+ this.active = true;
+ const keyupHandler = keyupEvent => {
+ if (this.__isKeyboardClickEvent(keyupEvent)) {
+ this.active = false;
+ document.removeEventListener('keyup', keyupHandler, true);
+ }
+ };
+ document.addEventListener('keyup', keyupHandler, true);
}
- __keyupDelegationHandler(e) {
- // Makes the real button the trigger in forms (will submit form, as opposed to paper-button)
- // and make click handlers on button work on space and enter
- if (e.keyCode === 32 /* space */ || e.keyCode === 13 /* enter */) {
- e.preventDefault();
- this.shadowRoot.querySelector('.btn').removeAttribute('active');
+ __keyupHandler(e) {
+ if (this.__isKeyboardClickEvent(e)) {
+ // redispatch click
this.shadowRoot.querySelector('.click-area').click();
}
}
+
+ // eslint-disable-next-line class-methods-use-this
+ __isKeyboardClickEvent(e) {
+ return e.keyCode === 32 /* space */ || e.keyCode === 13 /* enter */;
+ }
}
diff --git a/packages/button/stories/index.stories.js b/packages/button/stories/index.stories.js
index 5e8a9b7ff..0fa6c9d34 100644
--- a/packages/button/stories/index.stories.js
+++ b/packages/button/stories/index.stories.js
@@ -25,7 +25,9 @@ storiesOf('Buttons|Button', module)
Debug
Submit
- click/space/enter me
+ console.log('clicked/spaced/entered', e)}">
+ click/space/enter me and see log
+
Disabled
`,
diff --git a/packages/button/test/lion-button.test.js b/packages/button/test/lion-button.test.js
index 1ad5334b1..0b6792aa6 100644
--- a/packages/button/test/lion-button.test.js
+++ b/packages/button/test/lion-button.test.js
@@ -4,15 +4,20 @@ import {
makeMouseEvent,
pressEnter,
pressSpace,
+ down,
+ up,
+ keyDownOn,
+ keyUpOn,
} from '@polymer/iron-test-helpers/mock-interactions.js';
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', () => {
@@ -56,6 +61,98 @@ describe('lion-button', () => {
expect(el.hasAttribute('disabled')).to.equal(true);
});
+ describe('active', () => {
+ it('updates "active" attribute on host when mousedown/mouseup on button', async () => {
+ const el = await fixture(`foo`);
+ const topEl = getTopElement(el);
+
+ down(topEl);
+ expect(el.active).to.be.true;
+ await el.updateComplete;
+ expect(el.hasAttribute('active')).to.be.true;
+
+ up(topEl);
+ expect(el.active).to.be.false;
+ await el.updateComplete;
+ expect(el.hasAttribute('active')).to.be.false;
+ });
+
+ it('updates "active" attribute on host when mousedown on button and mouseup anywhere else', async () => {
+ const el = await fixture(`foo`);
+ const topEl = getTopElement(el);
+
+ down(topEl);
+ expect(el.active).to.be.true;
+ await el.updateComplete;
+ expect(el.hasAttribute('active')).to.be.true;
+
+ up(document.body);
+ expect(el.active).to.be.false;
+ await el.updateComplete;
+ expect(el.hasAttribute('active')).to.be.false;
+ });
+
+ it('updates "active" attribute on host when space keydown/keyup on button', async () => {
+ const el = await fixture(`foo`);
+ const topEl = getTopElement(el);
+
+ keyDownOn(topEl, 32);
+ expect(el.active).to.be.true;
+ await el.updateComplete;
+ expect(el.hasAttribute('active')).to.be.true;
+
+ keyUpOn(topEl, 32);
+ expect(el.active).to.be.false;
+ await el.updateComplete;
+ expect(el.hasAttribute('active')).to.be.false;
+ });
+
+ it('updates "active" attribute on host when space keydown on button and space keyup anywhere else', async () => {
+ const el = await fixture(`foo`);
+ const topEl = getTopElement(el);
+
+ keyDownOn(topEl, 32);
+ expect(el.active).to.be.true;
+ await el.updateComplete;
+ expect(el.hasAttribute('active')).to.be.true;
+
+ keyUpOn(document.body, 32);
+ expect(el.active).to.be.false;
+ await el.updateComplete;
+ expect(el.hasAttribute('active')).to.be.false;
+ });
+
+ it('updates "active" attribute on host when enter keydown/keyup on button', async () => {
+ const el = await fixture(`foo`);
+ const topEl = getTopElement(el);
+
+ keyDownOn(topEl, 13);
+ expect(el.active).to.be.true;
+ await el.updateComplete;
+ expect(el.hasAttribute('active')).to.be.true;
+
+ keyUpOn(topEl, 13);
+ expect(el.active).to.be.false;
+ await el.updateComplete;
+ expect(el.hasAttribute('active')).to.be.false;
+ });
+
+ it('updates "active" attribute on host when enter keydown on button and space keyup anywhere else', async () => {
+ const el = await fixture(`foo`);
+ const topEl = getTopElement(el);
+
+ keyDownOn(topEl, 13);
+ expect(el.active).to.be.true;
+ await el.updateComplete;
+ expect(el.hasAttribute('active')).to.be.true;
+
+ keyUpOn(document.body, 13);
+ expect(el.active).to.be.false;
+ await el.updateComplete;
+ expect(el.hasAttribute('active')).to.be.false;
+ });
+ });
+
describe('a11y', () => {
it('has a role="button" by default', async () => {
const el = await fixture(`foo`);
@@ -151,7 +248,7 @@ describe('lion-button', () => {
const clickSpy = sinon.spy();
const el = await fixture(
html`
-
+ foo
`,
);
@@ -164,27 +261,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 +286,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);
+ });
});
});
});
diff --git a/packages/popup/test/lion-popup.test.js b/packages/popup/test/lion-popup.test.js
index a8ead651d..9c6fd461a 100644
--- a/packages/popup/test/lion-popup.test.js
+++ b/packages/popup/test/lion-popup.test.js
@@ -22,12 +22,11 @@ describe('lion-popup', () => {
`);
const invoker = el.querySelector('[slot="invoker"]');
- const eventOnClick = new Event('click');
- invoker.dispatchEvent(eventOnClick);
+ invoker.click();
await el.updateComplete;
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('inline-block');
- invoker.dispatchEvent(eventOnClick);
+ invoker.click();
await el.updateComplete;
expect(el.querySelector('[slot="content"]').style.display).to.be.equal('none');
});