Skip to content

Commit f3d95cc

Browse files
authored
refactor: update LabelMixin to use SlotController (#3181)
1 parent 6b8dd6c commit f3d95cc

File tree

31 files changed

+584
-182
lines changed

31 files changed

+584
-182
lines changed

packages/checkbox/src/vaadin-checkbox.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,19 @@ class Checkbox extends SlotLabelMixin(
166166
}
167167

168168
/** @protected */
169-
ready() {
170-
super.ready();
169+
connectedCallback() {
170+
super.connectedCallback();
171171

172-
this.addController(
173-
new InputController(this, (input) => {
172+
if (!this._inputController) {
173+
this._inputController = new InputController(this, (input) => {
174174
this._setInputElement(input);
175175
this._setFocusElement(input);
176176
this.stateTarget = input;
177-
})
178-
);
179-
this.addController(new LabelledInputController(this.inputElement, this._labelNode));
177+
this.ariaTarget = input;
178+
});
179+
this.addController(this._inputController);
180+
this.addController(new LabelledInputController(this.inputElement, this._labelController));
181+
}
180182
}
181183

182184
/**

packages/checkbox/test/checkbox.test.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ describe('checkbox', () => {
2525
});
2626

2727
describe('default', () => {
28-
beforeEach(() => {
28+
beforeEach(async () => {
2929
checkbox = fixtureSync('<vaadin-checkbox>I accept <a href="#">the terms and conditions</a></vaadin-checkbox>');
30+
// Wait for MutationObserver.
31+
await nextFrame();
3032
input = checkbox.inputElement;
3133
label = checkbox._labelNode;
3234
link = label.children[0];
@@ -291,8 +293,9 @@ describe('checkbox', () => {
291293
console.warn.restore();
292294
});
293295

294-
it('should warn about using default slot label', () => {
296+
it('should warn about using default slot label', async () => {
295297
fixtureSync('<vaadin-checkbox>label</vaadin-checkbox>');
298+
await nextFrame();
296299

297300
expect(console.warn.calledOnce).to.be.true;
298301
expect(console.warn.args[0][0]).to.include(

packages/combo-box/src/vaadin-combo-box.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ class ComboBox extends ComboBoxDataProviderMixin(
244244
this.ariaTarget = input;
245245
})
246246
);
247-
this.addController(new LabelledInputController(this.inputElement, this._labelNode));
247+
this.addController(new LabelledInputController(this.inputElement, this._labelController));
248248
this._positionTarget = this.shadowRoot.querySelector('[part="input-field"]');
249249
this._toggleElement = this.$.toggleButton;
250250
}

packages/component-base/src/slot-controller.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66
import { ReactiveController } from 'lit';
77

8-
export class SlotController implements ReactiveController {
8+
export class SlotController extends EventTarget implements ReactiveController {
99
constructor(
1010
host: HTMLElement,
1111
slotName: string,
@@ -34,6 +34,12 @@ export class SlotController implements ReactiveController {
3434

3535
protected defaultNode: Node;
3636

37+
protected defaultId: string;
38+
39+
protected attachDefaultNode(): Node | undefined;
40+
41+
protected initNode(node: Node): void;
42+
3743
/**
3844
* Override to initialize the newly added custom node.
3945
*/

packages/component-base/src/slot-controller.js

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,44 +8,46 @@ import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nod
88
/**
99
* A controller for providing content to slot element and observing changes.
1010
*/
11-
export class SlotController {
11+
export class SlotController extends EventTarget {
1212
constructor(host, slotName, slotFactory, slotInitializer) {
13+
super();
14+
1315
this.host = host;
1416
this.slotName = slotName;
1517
this.slotFactory = slotFactory;
1618
this.slotInitializer = slotInitializer;
19+
this.defaultId = SlotController.generateId(slotName, host);
20+
}
21+
22+
/**
23+
* Ensure that every instance has unique ID.
24+
*
25+
* @param {string} slotName
26+
* @param {HTMLElement} host
27+
* @return {string}
28+
* @protected
29+
*/
30+
static generateId(slotName, host) {
31+
const prefix = slotName || 'default';
32+
33+
// Maintain the unique ID counter for a given prefix.
34+
this[`${prefix}Id`] = 1 + this[`${prefix}Id`] || 0;
35+
36+
return `${prefix}-${host.localName}-${this[`${prefix}Id`]}`;
1737
}
1838

1939
hostConnected() {
2040
if (!this.initialized) {
21-
const { host, slotName, slotFactory, slotInitializer } = this;
22-
23-
const slotted = this.getSlotChild();
24-
25-
if (!slotted) {
26-
// Slot factory is optional, some slots don't have default content.
27-
if (slotFactory) {
28-
const slotContent = slotFactory(host);
29-
if (slotContent instanceof Element) {
30-
if (slotName !== '') {
31-
slotContent.setAttribute('slot', slotName);
32-
}
33-
host.appendChild(slotContent);
34-
this.node = slotContent;
35-
36-
// Store reference to not pass default node to `initCustomNode`.
37-
this.defaultNode = slotContent;
38-
}
39-
}
41+
let node = this.getSlotChild();
42+
43+
if (!node) {
44+
node = this.attachDefaultNode();
4045
} else {
41-
this.node = slotted;
46+
this.node = node;
47+
this.initCustomNode(node);
4248
}
4349

44-
// Don't try to bind `this` to initializer (normally it's arrow function).
45-
// Instead, pass the host as a first argument to access component's state.
46-
if (slotInitializer) {
47-
slotInitializer(host, this.node);
48-
}
50+
this.initNode(node);
4951

5052
// TODO: Consider making this behavior opt-in to improve performance.
5153
this.observe();
@@ -54,6 +56,36 @@ export class SlotController {
5456
}
5557
}
5658

59+
/**
60+
* Create and attach default node using the slot factory.
61+
* @return {Node | undefined}
62+
* @protected
63+
*/
64+
attachDefaultNode() {
65+
const { host, slotName, slotFactory } = this;
66+
67+
// Check if the node was created previously and if so, reuse it.
68+
let node = this.defaultNode;
69+
70+
// Slot factory is optional, some slots don't have default content.
71+
if (!node && slotFactory) {
72+
node = slotFactory(host);
73+
if (node instanceof Element) {
74+
if (slotName !== '') {
75+
node.setAttribute('slot', slotName);
76+
}
77+
this.node = node;
78+
this.defaultNode = node;
79+
}
80+
}
81+
82+
if (node) {
83+
host.appendChild(node);
84+
}
85+
86+
return node;
87+
}
88+
5789
/**
5890
* Return a reference to the node managed by the controller.
5991
* @return {Node}
@@ -69,6 +101,19 @@ export class SlotController {
69101
});
70102
}
71103

104+
/**
105+
* @param {Node} node
106+
* @protected
107+
*/
108+
initNode(node) {
109+
const { slotInitializer } = this;
110+
// Don't try to bind `this` to initializer (normally it's arrow function).
111+
// Instead, pass the host as a first argument to access component's state.
112+
if (slotInitializer) {
113+
slotInitializer(this.host, node);
114+
}
115+
}
116+
72117
/**
73118
* Override to initialize the newly added custom node.
74119
*
@@ -115,6 +160,8 @@ export class SlotController {
115160

116161
if (newNode !== this.defaultNode) {
117162
this.initCustomNode(newNode);
163+
164+
this.initNode(newNode);
118165
}
119166
}
120167
});

packages/date-picker/src/vaadin-date-picker.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ class DatePicker extends DatePickerMixin(InputControlMixin(ThemableMixin(Element
212212
this.ariaTarget = input;
213213
})
214214
);
215-
this.addController(new LabelledInputController(this.inputElement, this._labelNode));
215+
this.addController(new LabelledInputController(this.inputElement, this._labelController));
216216
addListener(this.shadowRoot.querySelector('[part="toggle-button"]'), 'tap', this._toggle.bind(this));
217217
}
218218

packages/email-field/test/dom/__snapshots__/email-field.test.snap.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,7 @@ snapshots["vaadin-email-field shadow theme"] =
229229
/* end snapshot vaadin-email-field shadow theme */
230230

231231
snapshots["vaadin-email-field slots default"] =
232-
`<label slot="label">
233-
</label>
234-
<div
232+
`<div
235233
hidden=""
236234
slot="error-message"
237235
>
@@ -241,13 +239,13 @@ snapshots["vaadin-email-field slots default"] =
241239
slot="input"
242240
type="email"
243241
>
242+
<label slot="label">
243+
</label>
244244
`;
245245
/* end snapshot vaadin-email-field slots default */
246246

247247
snapshots["vaadin-email-field slots helper"] =
248-
`<label slot="label">
249-
</label>
250-
<div
248+
`<div
251249
hidden=""
252250
slot="error-message"
253251
>
@@ -257,6 +255,8 @@ snapshots["vaadin-email-field slots helper"] =
257255
slot="input"
258256
type="email"
259257
>
258+
<label slot="label">
259+
</label>
260260
<div slot="helper">
261261
Helper
262262
</div>

packages/field-base/src/field-mixin.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
import { Constructor } from '@open-wc/dedupe-mixin';
77
import { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js';
8+
import { SlotMixinClass } from '@vaadin/component-base/src/slot-mixin.js';
89
import { LabelMixinClass } from './label-mixin.js';
910
import { ValidateMixinClass } from './validate-mixin.js';
1011

@@ -17,6 +18,7 @@ export declare function FieldMixin<T extends Constructor<HTMLElement>>(
1718
Constructor<ControllerMixinClass> &
1819
Constructor<FieldMixinClass> &
1920
Constructor<LabelMixinClass> &
21+
Constructor<SlotMixinClass> &
2022
Constructor<ValidateMixinClass>;
2123

2224
export declare class FieldMixinClass {

packages/field-base/src/field-mixin.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
77
import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
8+
import { SlotMixin } from '@vaadin/component-base/src/slot-mixin.js';
89
import { FieldAriaController } from './field-aria-controller.js';
910
import { LabelMixin } from './label-mixin.js';
1011
import { ValidateMixin } from './validate-mixin.js';
@@ -15,10 +16,11 @@ import { ValidateMixin } from './validate-mixin.js';
1516
* @polymerMixin
1617
* @mixes ControllerMixin
1718
* @mixes LabelMixin
19+
* @mixes SlotMixin
1820
* @mixes ValidateMixin
1921
*/
2022
export const FieldMixin = (superclass) =>
21-
class FieldMixinClass extends ValidateMixin(LabelMixin(ControllerMixin(superclass))) {
23+
class FieldMixinClass extends ValidateMixin(LabelMixin(ControllerMixin(SlotMixin(superclass)))) {
2224
static get properties() {
2325
return {
2426
/**
@@ -102,6 +104,11 @@ export const FieldMixin = (superclass) =>
102104
this.__savedHelperId = this._helperId;
103105

104106
this._fieldAriaController = new FieldAriaController(this);
107+
108+
this._labelController.addEventListener('label-changed', (event) => {
109+
const { hasLabel, node } = event.detail;
110+
this.__labelChanged(hasLabel, node);
111+
});
105112
}
106113

107114
/** @protected */
@@ -240,17 +247,12 @@ export const FieldMixin = (superclass) =>
240247
this.toggleAttribute('has-helper', hasHelper);
241248
}
242249

243-
/**
244-
* @protected
245-
* @override
246-
*/
247-
_toggleHasLabelAttribute() {
248-
super._toggleHasLabelAttribute();
249-
250+
/** @private */
251+
__labelChanged(hasLabel, labelNode) {
250252
// Label ID should be only added when the label content is present.
251253
// Otherwise, it may conflict with an `aria-label` attribute possibly added by the user.
252-
if (this.hasAttribute('has-label')) {
253-
this._fieldAriaController.setLabelId(this._labelId);
254+
if (hasLabel) {
255+
this._fieldAriaController.setLabelId(labelNode.id);
254256
} else {
255257
this._fieldAriaController.setLabelId(null);
256258
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2021 Vaadin Ltd.
4+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5+
*/
6+
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
7+
8+
/**
9+
* A controller to manage the label element.
10+
*/
11+
export class LabelController extends SlotController {
12+
/**
13+
* String used for the label.
14+
*/
15+
protected label: string | null | undefined;
16+
17+
/**
18+
* Set label based on corresponding host property.
19+
*/
20+
setLabel(label: string | null | undefined): void;
21+
22+
/**
23+
* ID attribute value set on the label element.
24+
*/
25+
labelId: string;
26+
}

0 commit comments

Comments
 (0)