Skip to content

Commit 88f5264

Browse files
daKmoRJoren Broekema
authored andcommitted
fix(field): no delegate in FocusMixin; sync focused, redispatch events
1 parent 6a4931e commit 88f5264

File tree

3 files changed

+213
-69
lines changed

3 files changed

+213
-69
lines changed

packages/field/src/FocusMixin.js

Lines changed: 96 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,120 @@
1-
import { dedupeMixin, DelegateMixin } from '@lion/core';
1+
import { dedupeMixin } from '@lion/core';
22

33
export const FocusMixin = dedupeMixin(
44
superclass =>
55
// eslint-disable-next-line no-unused-vars, max-len, no-shadow
6-
class FocusMixin extends DelegateMixin(superclass) {
7-
get delegations() {
6+
class FocusMixin extends superclass {
7+
static get properties() {
88
return {
9-
...super.delegations,
10-
target: () => this.inputElement,
11-
events: [...super.delegations.events, 'focus', 'blur'], // since these events don't bubble
12-
methods: [...super.delegations.methods, 'focus', 'blur'],
13-
properties: [...super.delegations.properties, 'onfocus', 'onblur', 'autofocus'],
14-
attributes: [...super.delegations.attributes, 'onfocus', 'onblur', 'autofocus'],
9+
focused: {
10+
type: Boolean,
11+
reflect: true,
12+
},
1513
};
1614
}
1715

16+
constructor() {
17+
super();
18+
this.focused = false;
19+
}
20+
1821
connectedCallback() {
19-
super.connectedCallback();
20-
this._onFocus = this._onFocus.bind(this);
21-
this._onBlur = this._onBlur.bind(this);
22-
this.inputElement.addEventListener('focusin', this._onFocus);
23-
this.inputElement.addEventListener('focusout', this._onBlur);
22+
if (super.connectedCallback) {
23+
super.connectedCallback();
24+
}
25+
this.__registerEventsForFocusMixin();
2426
}
2527

2628
disconnectedCallback() {
27-
super.disconnectedCallback();
28-
this.inputElement.removeEventListener('focusin', this._onFocus);
29-
this.inputElement.removeEventListener('focusout', this._onBlur);
29+
if (super.disconnectedCallback) {
30+
super.disconnectedCallback();
31+
}
32+
this.__teardownEventsForFocusMixin();
33+
}
34+
35+
focus() {
36+
const native = this.inputElement;
37+
if (native) {
38+
native.focus();
39+
}
40+
}
41+
42+
blur() {
43+
const native = this.inputElement;
44+
if (native) {
45+
native.blur();
46+
}
47+
}
48+
49+
updated(changedProperties) {
50+
super.updated(changedProperties);
51+
// 'state-focused' css classes are deprecated
52+
if (changedProperties.has('focused')) {
53+
this.classList[this.focused ? 'add' : 'remove']('state-focused');
54+
}
3055
}
3156

3257
/**
33-
* Helper Function to easily check if the element is being focused
58+
* Functions should be private
3459
*
35-
* TODO: performance comparision vs
36-
* return this.inputElement === document.activeElement;
60+
* @deprecated
3761
*/
38-
get focused() {
39-
return this.classList.contains('state-focused');
40-
}
41-
4262
_onFocus() {
43-
if (super._onFocus) super._onFocus();
44-
this.classList.add('state-focused');
63+
if (super._onFocus) {
64+
super._onFocus();
65+
}
66+
this.focused = true;
4567
}
4668

69+
/**
70+
* Functions should be private
71+
*
72+
* @deprecated
73+
*/
4774
_onBlur() {
48-
if (super._onBlur) super._onBlur();
49-
this.classList.remove('state-focused');
75+
if (super._onBlur) {
76+
super._onBlur();
77+
}
78+
this.focused = false;
79+
}
80+
81+
__registerEventsForFocusMixin() {
82+
// focus
83+
this.__redispatchFocus = ev => {
84+
ev.stopPropagation();
85+
this.dispatchEvent(new FocusEvent('focus'));
86+
};
87+
this.inputElement.addEventListener('focus', this.__redispatchFocus);
88+
89+
// blur
90+
this.__redispatchBlur = ev => {
91+
ev.stopPropagation();
92+
this.dispatchEvent(new FocusEvent('blur'));
93+
};
94+
this.inputElement.addEventListener('blur', this.__redispatchBlur);
95+
96+
// focusin
97+
this.__redispatchFocusin = ev => {
98+
ev.stopPropagation();
99+
this._onFocus(ev);
100+
this.dispatchEvent(new FocusEvent('focusin', { bubbles: true, composed: true }));
101+
};
102+
this.inputElement.addEventListener('focusin', this.__redispatchFocusin);
103+
104+
// focusout
105+
this.__redispatchFocusout = ev => {
106+
ev.stopPropagation();
107+
this._onBlur();
108+
this.dispatchEvent(new FocusEvent('focusout', { bubbles: true, composed: true }));
109+
};
110+
this.inputElement.addEventListener('focusout', this.__redispatchFocusout);
111+
}
112+
113+
__teardownEventsForFocusMixin() {
114+
this.inputElement.removeEventListener('focus', this.__redispatchFocus);
115+
this.inputElement.removeEventListener('blur', this.__redispatchBlur);
116+
this.inputElement.removeEventListener('focusin', this.__redispatchFocusin);
117+
this.inputElement.removeEventListener('focusout', this.__redispatchFocusout);
50118
}
51119
},
52120
);
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { expect, fixture, html, defineCE, unsafeStatic, oneEvent } from '@open-wc/testing';
2+
3+
import { LitElement } from '@lion/core';
4+
import { FocusMixin } from '../src/FocusMixin.js';
5+
6+
describe('FocusMixin', () => {
7+
let tag;
8+
9+
before(async () => {
10+
const tagString = defineCE(
11+
class extends FocusMixin(LitElement) {
12+
render() {
13+
return html`
14+
<slot name="input"></slot>
15+
`;
16+
}
17+
18+
get inputElement() {
19+
return this.querySelector('input');
20+
}
21+
},
22+
);
23+
24+
tag = unsafeStatic(tagString);
25+
});
26+
27+
it('focuses/blurs the underlaying native element on .focus()/.blur()', async () => {
28+
const el = await fixture(html`
29+
<${tag}><input slot="input"></${tag}>
30+
`);
31+
el.focus();
32+
expect(document.activeElement === el.inputElement).to.be.true;
33+
el.blur();
34+
expect(document.activeElement === el.inputElement).to.be.false;
35+
});
36+
37+
it('has an attribute focused when focused', async () => {
38+
const el = await fixture(html`
39+
<${tag}><input slot="input"></${tag}>
40+
`);
41+
el.focus();
42+
await el.updateComplete;
43+
expect(el.hasAttribute('focused')).to.be.true;
44+
45+
el.blur();
46+
await el.updateComplete;
47+
expect(el.hasAttribute('focused')).to.be.false;
48+
});
49+
50+
it('becomes focused/blurred if the native element gets focused/blurred', async () => {
51+
const el = await fixture(html`
52+
<${tag}><input slot="input"></${tag}>
53+
`);
54+
expect(el.focused).to.be.false;
55+
el.inputElement.focus();
56+
expect(el.focused).to.be.true;
57+
el.inputElement.blur();
58+
expect(el.focused).to.be.false;
59+
});
60+
61+
it('has a deprecated "state-focused" css class when focused', async () => {
62+
const el = await fixture(html`
63+
<${tag}><input slot="input"></${tag}>
64+
`);
65+
el.focus();
66+
await el.updateComplete;
67+
expect(el.classList.contains('state-focused')).to.be.true;
68+
69+
el.blur();
70+
await el.updateComplete;
71+
expect(el.classList.contains('state-focused')).to.be.false;
72+
});
73+
74+
it('dispatches [focus, blur] events', async () => {
75+
const el = await fixture(html`
76+
<${tag}><input slot="input"></${tag}>
77+
`);
78+
setTimeout(() => el.focus());
79+
const focusEv = await oneEvent(el, 'focus');
80+
expect(focusEv).to.be.instanceOf(FocusEvent);
81+
expect(focusEv.target).to.equal(el);
82+
expect(focusEv.bubbles).to.be.false;
83+
expect(focusEv.composed).to.be.false;
84+
85+
setTimeout(() => {
86+
el.focus();
87+
el.blur();
88+
});
89+
const blurEv = await oneEvent(el, 'blur');
90+
expect(blurEv).to.be.instanceOf(FocusEvent);
91+
expect(blurEv.target).to.equal(el);
92+
expect(blurEv.bubbles).to.be.false;
93+
expect(blurEv.composed).to.be.false;
94+
});
95+
96+
it('dispatches [focusin, focusout] events with { bubbles: true, composed: true }', async () => {
97+
const el = await fixture(html`
98+
<${tag}><input slot="input"></${tag}>
99+
`);
100+
setTimeout(() => el.focus());
101+
const focusinEv = await oneEvent(el, 'focusin');
102+
expect(focusinEv).to.be.instanceOf(FocusEvent);
103+
expect(focusinEv.target).to.equal(el);
104+
expect(focusinEv.bubbles).to.be.true;
105+
expect(focusinEv.composed).to.be.true;
106+
107+
setTimeout(() => {
108+
el.focus();
109+
el.blur();
110+
});
111+
const focusoutEv = await oneEvent(el, 'focusout');
112+
expect(focusoutEv).to.be.instanceOf(FocusEvent);
113+
expect(focusoutEv.target).to.equal(el);
114+
expect(focusoutEv.bubbles).to.be.true;
115+
expect(focusoutEv.composed).to.be.true;
116+
});
117+
});

packages/field/test/lion-field.test.js

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -68,24 +68,6 @@ describe('<lion-field>', () => {
6868
expect(cbBlurNativeInput.callCount).to.equal(2);
6969
});
7070

71-
it('has class "state-focused" if focused', async () => {
72-
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
73-
expect(el.classList.contains('state-focused')).to.equal(false, 'no state-focused initially');
74-
await triggerFocusFor(el.inputElement);
75-
expect(el.classList.contains('state-focused')).to.equal(true, 'state-focused after focus()');
76-
await triggerBlurFor(el.inputElement);
77-
expect(el.classList.contains('state-focused')).to.equal(false, 'no state-focused after blur()');
78-
});
79-
80-
it('offers simple getter "this.focused" returning true/false for the current focus state', async () => {
81-
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
82-
expect(el.focused).to.equal(false);
83-
await triggerFocusFor(el);
84-
expect(el.focused).to.equal(true);
85-
await triggerBlurFor(el);
86-
expect(el.focused).to.equal(false);
87-
});
88-
8971
it('can be disabled via attribute', async () => {
9072
const elDisabled = await fixture(`<${tagString} disabled>${inputSlotString}</${tagString}>`);
9173
expect(elDisabled.disabled).to.equal(true);
@@ -395,12 +377,6 @@ describe('<lion-field>', () => {
395377
});
396378

397379
describe(`Delegation${nameSuffix}`, () => {
398-
it('delegates attribute autofocus', async () => {
399-
const el = await fixture(`<${tagString} autofocus>${inputSlotString}</${tagString}>`);
400-
expect(el.hasAttribute('autofocus')).to.be.false;
401-
expect(el.inputElement.hasAttribute('autofocus')).to.be.true;
402-
});
403-
404380
it('delegates property value', async () => {
405381
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
406382
expect(el.inputElement.value).to.equal('');
@@ -426,23 +402,6 @@ describe('<lion-field>', () => {
426402
}
427403
});
428404

429-
it('delegates property onfocus', async () => {
430-
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
431-
const cbFocusHost = sinon.spy();
432-
el.onfocus = cbFocusHost;
433-
await triggerFocusFor(el.inputElement);
434-
expect(cbFocusHost.callCount).to.equal(1);
435-
});
436-
437-
it('delegates property onblur', async () => {
438-
const el = await fixture(`<${tagString}>${inputSlotString}</${tagString}>`);
439-
const cbBlurHost = sinon.spy();
440-
el.onblur = cbBlurHost;
441-
await triggerFocusFor(el.inputElement);
442-
await triggerBlurFor(el.inputElement);
443-
expect(cbBlurHost.callCount).to.equal(1);
444-
});
445-
446405
it('delegates property selectionStart and selectionEnd', async () => {
447406
const el = await fixture(html`
448407
<${tag}

0 commit comments

Comments
 (0)