fix(field): no delegate in FocusMixin; sync focused, redispatch events
This commit is contained in:
parent
6a4931e74c
commit
88f52646b8
3 changed files with 213 additions and 69 deletions
|
|
@ -1,52 +1,120 @@
|
||||||
import { dedupeMixin, DelegateMixin } from '@lion/core';
|
import { dedupeMixin } from '@lion/core';
|
||||||
|
|
||||||
export const FocusMixin = dedupeMixin(
|
export const FocusMixin = dedupeMixin(
|
||||||
superclass =>
|
superclass =>
|
||||||
// eslint-disable-next-line no-unused-vars, max-len, no-shadow
|
// eslint-disable-next-line no-unused-vars, max-len, no-shadow
|
||||||
class FocusMixin extends DelegateMixin(superclass) {
|
class FocusMixin extends superclass {
|
||||||
get delegations() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
...super.delegations,
|
focused: {
|
||||||
target: () => this.inputElement,
|
type: Boolean,
|
||||||
events: [...super.delegations.events, 'focus', 'blur'], // since these events don't bubble
|
reflect: true,
|
||||||
methods: [...super.delegations.methods, 'focus', 'blur'],
|
},
|
||||||
properties: [...super.delegations.properties, 'onfocus', 'onblur', 'autofocus'],
|
|
||||||
attributes: [...super.delegations.attributes, 'onfocus', 'onblur', 'autofocus'],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.focused = false;
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
if (super.connectedCallback) {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._onFocus = this._onFocus.bind(this);
|
}
|
||||||
this._onBlur = this._onBlur.bind(this);
|
this.__registerEventsForFocusMixin();
|
||||||
this.inputElement.addEventListener('focusin', this._onFocus);
|
|
||||||
this.inputElement.addEventListener('focusout', this._onBlur);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
if (super.disconnectedCallback) {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.inputElement.removeEventListener('focusin', this._onFocus);
|
}
|
||||||
this.inputElement.removeEventListener('focusout', this._onBlur);
|
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
|
* @deprecated
|
||||||
* return this.inputElement === document.activeElement;
|
|
||||||
*/
|
*/
|
||||||
get focused() {
|
|
||||||
return this.classList.contains('state-focused');
|
|
||||||
}
|
|
||||||
|
|
||||||
_onFocus() {
|
_onFocus() {
|
||||||
if (super._onFocus) super._onFocus();
|
if (super._onFocus) {
|
||||||
this.classList.add('state-focused');
|
super._onFocus();
|
||||||
|
}
|
||||||
|
this.focused = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions should be private
|
||||||
|
*
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
_onBlur() {
|
_onBlur() {
|
||||||
if (super._onBlur) super._onBlur();
|
if (super._onBlur) {
|
||||||
this.classList.remove('state-focused');
|
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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
117
packages/field/test/FocusMixin.test.js
Normal file
117
packages/field/test/FocusMixin.test.js
Normal file
|
|
@ -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`
|
||||||
|
<slot name="input"></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}><input slot="input"></${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}><input slot="input"></${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}><input slot="input"></${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}><input slot="input"></${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}><input slot="input"></${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}><input slot="input"></${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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -68,24 +68,6 @@ describe('<lion-field>', () => {
|
||||||
expect(cbBlurNativeInput.callCount).to.equal(2);
|
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 () => {
|
it('can be disabled via attribute', async () => {
|
||||||
const elDisabled = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`);
|
const elDisabled = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`);
|
||||||
expect(elDisabled.disabled).to.equal(true);
|
expect(elDisabled.disabled).to.equal(true);
|
||||||
|
|
@ -395,12 +377,6 @@ describe('<lion-field>', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`Delegation${nameSuffix}`, () => {
|
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 () => {
|
it('delegates property value', async () => {
|
||||||
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
|
||||||
expect(el.inputElement.value).to.equal('');
|
expect(el.inputElement.value).to.equal('');
|
||||||
|
|
@ -426,23 +402,6 @@ describe('<lion-field>', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
it('delegates property selectionStart and selectionEnd', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag}
|
<${tag}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue