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(
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
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);
|
||||
});
|
||||
|
||||
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('<lion-field>', () => {
|
|||
});
|
||||
|
||||
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('<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 () => {
|
||||
const el = await fixture(html`
|
||||
<${tag}
|
||||
|
|
|
|||
Loading…
Reference in a new issue