diff --git a/packages/field/src/FocusMixin.js b/packages/field/src/FocusMixin.js
index 17f33bc81..2837c05dd 100644
--- a/packages/field/src/FocusMixin.js
+++ b/packages/field/src/FocusMixin.js
@@ -1,52 +1,120 @@
-import { dedupeMixin, DelegateMixin } from '@lion/core';
+import { dedupeMixin } from '@lion/core';
export const FocusMixin = dedupeMixin(
superclass =>
// eslint-disable-next-line no-unused-vars, max-len, no-shadow
- class FocusMixin extends DelegateMixin(superclass) {
- get delegations() {
+ class FocusMixin extends superclass {
+ static get properties() {
return {
- ...super.delegations,
- target: () => this.inputElement,
- events: [...super.delegations.events, 'focus', 'blur'], // since these events don't bubble
- methods: [...super.delegations.methods, 'focus', 'blur'],
- properties: [...super.delegations.properties, 'onfocus', 'onblur', 'autofocus'],
- attributes: [...super.delegations.attributes, 'onfocus', 'onblur', 'autofocus'],
+ focused: {
+ type: Boolean,
+ reflect: true,
+ },
};
}
+ constructor() {
+ super();
+ this.focused = false;
+ }
+
connectedCallback() {
- super.connectedCallback();
- this._onFocus = this._onFocus.bind(this);
- this._onBlur = this._onBlur.bind(this);
- this.inputElement.addEventListener('focusin', this._onFocus);
- this.inputElement.addEventListener('focusout', this._onBlur);
+ if (super.connectedCallback) {
+ super.connectedCallback();
+ }
+ this.__registerEventsForFocusMixin();
}
disconnectedCallback() {
- super.disconnectedCallback();
- this.inputElement.removeEventListener('focusin', this._onFocus);
- this.inputElement.removeEventListener('focusout', this._onBlur);
+ if (super.disconnectedCallback) {
+ super.disconnectedCallback();
+ }
+ this.__teardownEventsForFocusMixin();
+ }
+
+ focus() {
+ const native = this.inputElement;
+ if (native) {
+ native.focus();
+ }
+ }
+
+ blur() {
+ const native = this.inputElement;
+ if (native) {
+ native.blur();
+ }
+ }
+
+ updated(changedProperties) {
+ super.updated(changedProperties);
+ // 'state-focused' css classes are deprecated
+ if (changedProperties.has('focused')) {
+ this.classList[this.focused ? 'add' : 'remove']('state-focused');
+ }
}
/**
- * Helper Function to easily check if the element is being focused
+ * Functions should be private
*
- * TODO: performance comparision vs
- * return this.inputElement === document.activeElement;
+ * @deprecated
*/
- get focused() {
- return this.classList.contains('state-focused');
- }
-
_onFocus() {
- if (super._onFocus) super._onFocus();
- this.classList.add('state-focused');
+ if (super._onFocus) {
+ super._onFocus();
+ }
+ this.focused = true;
}
+ /**
+ * Functions should be private
+ *
+ * @deprecated
+ */
_onBlur() {
- if (super._onBlur) super._onBlur();
- this.classList.remove('state-focused');
+ if (super._onBlur) {
+ super._onBlur();
+ }
+ this.focused = false;
+ }
+
+ __registerEventsForFocusMixin() {
+ // focus
+ this.__redispatchFocus = ev => {
+ ev.stopPropagation();
+ this.dispatchEvent(new FocusEvent('focus'));
+ };
+ this.inputElement.addEventListener('focus', this.__redispatchFocus);
+
+ // blur
+ this.__redispatchBlur = ev => {
+ ev.stopPropagation();
+ this.dispatchEvent(new FocusEvent('blur'));
+ };
+ this.inputElement.addEventListener('blur', this.__redispatchBlur);
+
+ // focusin
+ this.__redispatchFocusin = ev => {
+ ev.stopPropagation();
+ this._onFocus(ev);
+ this.dispatchEvent(new FocusEvent('focusin', { bubbles: true, composed: true }));
+ };
+ this.inputElement.addEventListener('focusin', this.__redispatchFocusin);
+
+ // focusout
+ this.__redispatchFocusout = ev => {
+ ev.stopPropagation();
+ this._onBlur();
+ this.dispatchEvent(new FocusEvent('focusout', { bubbles: true, composed: true }));
+ };
+ this.inputElement.addEventListener('focusout', this.__redispatchFocusout);
+ }
+
+ __teardownEventsForFocusMixin() {
+ this.inputElement.removeEventListener('focus', this.__redispatchFocus);
+ this.inputElement.removeEventListener('blur', this.__redispatchBlur);
+ this.inputElement.removeEventListener('focusin', this.__redispatchFocusin);
+ this.inputElement.removeEventListener('focusout', this.__redispatchFocusout);
}
},
);
diff --git a/packages/field/test/FocusMixin.test.js b/packages/field/test/FocusMixin.test.js
new file mode 100644
index 000000000..e2085f2cc
--- /dev/null
+++ b/packages/field/test/FocusMixin.test.js
@@ -0,0 +1,117 @@
+import { expect, fixture, html, defineCE, unsafeStatic, oneEvent } from '@open-wc/testing';
+
+import { LitElement } from '@lion/core';
+import { FocusMixin } from '../src/FocusMixin.js';
+
+describe('FocusMixin', () => {
+ let tag;
+
+ before(async () => {
+ const tagString = defineCE(
+ class extends FocusMixin(LitElement) {
+ render() {
+ return html`
+
+ `;
+ }
+
+ get inputElement() {
+ return this.querySelector('input');
+ }
+ },
+ );
+
+ tag = unsafeStatic(tagString);
+ });
+
+ it('focuses/blurs the underlaying native element on .focus()/.blur()', async () => {
+ const el = await fixture(html`
+ <${tag}>${tag}>
+ `);
+ el.focus();
+ expect(document.activeElement === el.inputElement).to.be.true;
+ el.blur();
+ expect(document.activeElement === el.inputElement).to.be.false;
+ });
+
+ it('has an attribute focused when focused', async () => {
+ const el = await fixture(html`
+ <${tag}>${tag}>
+ `);
+ el.focus();
+ await el.updateComplete;
+ expect(el.hasAttribute('focused')).to.be.true;
+
+ el.blur();
+ await el.updateComplete;
+ expect(el.hasAttribute('focused')).to.be.false;
+ });
+
+ it('becomes focused/blurred if the native element gets focused/blurred', async () => {
+ const el = await fixture(html`
+ <${tag}>${tag}>
+ `);
+ expect(el.focused).to.be.false;
+ el.inputElement.focus();
+ expect(el.focused).to.be.true;
+ el.inputElement.blur();
+ expect(el.focused).to.be.false;
+ });
+
+ it('has a deprecated "state-focused" css class when focused', async () => {
+ const el = await fixture(html`
+ <${tag}>${tag}>
+ `);
+ el.focus();
+ await el.updateComplete;
+ expect(el.classList.contains('state-focused')).to.be.true;
+
+ el.blur();
+ await el.updateComplete;
+ expect(el.classList.contains('state-focused')).to.be.false;
+ });
+
+ it('dispatches [focus, blur] events', async () => {
+ const el = await fixture(html`
+ <${tag}>${tag}>
+ `);
+ setTimeout(() => el.focus());
+ const focusEv = await oneEvent(el, 'focus');
+ expect(focusEv).to.be.instanceOf(FocusEvent);
+ expect(focusEv.target).to.equal(el);
+ expect(focusEv.bubbles).to.be.false;
+ expect(focusEv.composed).to.be.false;
+
+ setTimeout(() => {
+ el.focus();
+ el.blur();
+ });
+ const blurEv = await oneEvent(el, 'blur');
+ expect(blurEv).to.be.instanceOf(FocusEvent);
+ expect(blurEv.target).to.equal(el);
+ expect(blurEv.bubbles).to.be.false;
+ expect(blurEv.composed).to.be.false;
+ });
+
+ it('dispatches [focusin, focusout] events with { bubbles: true, composed: true }', async () => {
+ const el = await fixture(html`
+ <${tag}>${tag}>
+ `);
+ setTimeout(() => el.focus());
+ const focusinEv = await oneEvent(el, 'focusin');
+ expect(focusinEv).to.be.instanceOf(FocusEvent);
+ expect(focusinEv.target).to.equal(el);
+ expect(focusinEv.bubbles).to.be.true;
+ expect(focusinEv.composed).to.be.true;
+
+ setTimeout(() => {
+ el.focus();
+ el.blur();
+ });
+ const focusoutEv = await oneEvent(el, 'focusout');
+ expect(focusoutEv).to.be.instanceOf(FocusEvent);
+ expect(focusoutEv.target).to.equal(el);
+ expect(focusoutEv.bubbles).to.be.true;
+ expect(focusoutEv.composed).to.be.true;
+ });
+});
diff --git a/packages/field/test/lion-field.test.js b/packages/field/test/lion-field.test.js
index c0adcf008..d35988db4 100644
--- a/packages/field/test/lion-field.test.js
+++ b/packages/field/test/lion-field.test.js
@@ -68,24 +68,6 @@ describe('', () => {
expect(cbBlurNativeInput.callCount).to.equal(2);
});
- it('has class "state-focused" if focused', async () => {
- const el = await fixture(`<${tagString}>${inputSlotString}${tagString}>`);
- expect(el.classList.contains('state-focused')).to.equal(false, 'no state-focused initially');
- await triggerFocusFor(el.inputElement);
- expect(el.classList.contains('state-focused')).to.equal(true, 'state-focused after focus()');
- await triggerBlurFor(el.inputElement);
- expect(el.classList.contains('state-focused')).to.equal(false, 'no state-focused after blur()');
- });
-
- it('offers simple getter "this.focused" returning true/false for the current focus state', async () => {
- const el = await fixture(`<${tagString}>${inputSlotString}${tagString}>`);
- expect(el.focused).to.equal(false);
- await triggerFocusFor(el);
- expect(el.focused).to.equal(true);
- await triggerBlurFor(el);
- expect(el.focused).to.equal(false);
- });
-
it('can be disabled via attribute', async () => {
const elDisabled = await fixture(`<${tagString} disabled>${inputSlotString}${tagString}>`);
expect(elDisabled.disabled).to.equal(true);
@@ -395,12 +377,6 @@ describe('', () => {
});
describe(`Delegation${nameSuffix}`, () => {
- it('delegates attribute autofocus', async () => {
- const el = await fixture(`<${tagString} autofocus>${inputSlotString}${tagString}>`);
- expect(el.hasAttribute('autofocus')).to.be.false;
- expect(el.inputElement.hasAttribute('autofocus')).to.be.true;
- });
-
it('delegates property value', async () => {
const el = await fixture(`<${tagString}>${inputSlotString}${tagString}>`);
expect(el.inputElement.value).to.equal('');
@@ -426,23 +402,6 @@ describe('', () => {
}
});
- it('delegates property onfocus', async () => {
- const el = await fixture(`<${tagString}>${inputSlotString}${tagString}>`);
- const cbFocusHost = sinon.spy();
- el.onfocus = cbFocusHost;
- await triggerFocusFor(el.inputElement);
- expect(cbFocusHost.callCount).to.equal(1);
- });
-
- it('delegates property onblur', async () => {
- const el = await fixture(`<${tagString}>${inputSlotString}${tagString}>`);
- const cbBlurHost = sinon.spy();
- el.onblur = cbBlurHost;
- await triggerFocusFor(el.inputElement);
- await triggerBlurFor(el.inputElement);
- expect(cbBlurHost.callCount).to.equal(1);
- });
-
it('delegates property selectionStart and selectionEnd', async () => {
const el = await fixture(html`
<${tag}