@vaadin/field-base 22.0.1 → 23.0.0-alpha3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +1 -1
  2. package/package.json +3 -3
  3. package/src/checked-mixin.d.ts +1 -1
  4. package/src/checked-mixin.js +1 -1
  5. package/src/delegate-focus-mixin.d.ts +1 -1
  6. package/src/delegate-focus-mixin.js +1 -1
  7. package/src/delegate-state-mixin.d.ts +1 -1
  8. package/src/delegate-state-mixin.js +1 -1
  9. package/src/field-aria-controller.d.ts +1 -1
  10. package/src/field-aria-controller.js +5 -8
  11. package/src/field-mixin.d.ts +3 -1
  12. package/src/field-mixin.js +48 -192
  13. package/src/helper-controller.d.ts +23 -0
  14. package/src/helper-controller.js +184 -0
  15. package/src/input-constraints-mixin.d.ts +1 -1
  16. package/src/input-constraints-mixin.js +2 -3
  17. package/src/input-control-mixin.d.ts +1 -1
  18. package/src/input-control-mixin.js +1 -1
  19. package/src/input-controller.d.ts +3 -3
  20. package/src/input-controller.js +5 -4
  21. package/src/input-field-mixin.d.ts +1 -1
  22. package/src/input-field-mixin.js +1 -1
  23. package/src/input-mixin.d.ts +1 -1
  24. package/src/input-mixin.js +1 -1
  25. package/src/label-controller.d.ts +26 -0
  26. package/src/label-controller.js +186 -0
  27. package/src/label-mixin.d.ts +4 -3
  28. package/src/label-mixin.js +10 -49
  29. package/src/labelled-input-controller.d.ts +1 -1
  30. package/src/labelled-input-controller.js +17 -4
  31. package/src/pattern-mixin.d.ts +1 -1
  32. package/src/pattern-mixin.js +1 -2
  33. package/src/shadow-focus-mixin.d.ts +1 -1
  34. package/src/shadow-focus-mixin.js +1 -1
  35. package/src/slot-label-mixin.d.ts +1 -1
  36. package/src/slot-label-mixin.js +1 -14
  37. package/src/slot-styles-mixin.d.ts +1 -1
  38. package/src/slot-styles-mixin.js +1 -1
  39. package/src/slot-target-mixin.d.ts +1 -1
  40. package/src/slot-target-mixin.js +1 -1
  41. package/src/styles/clear-button-styles.d.ts +1 -1
  42. package/src/styles/clear-button-styles.js +1 -1
  43. package/src/styles/field-shared-styles.d.ts +1 -1
  44. package/src/styles/field-shared-styles.js +1 -1
  45. package/src/styles/input-field-container-styles.d.ts +1 -1
  46. package/src/styles/input-field-container-styles.js +1 -1
  47. package/src/styles/input-field-shared-styles.d.ts +1 -1
  48. package/src/styles/input-field-shared-styles.js +1 -1
  49. package/src/text-area-controller.d.ts +3 -3
  50. package/src/text-area-controller.js +5 -4
  51. package/src/utils.d.ts +16 -0
  52. package/src/utils.js +56 -0
  53. package/src/validate-mixin.d.ts +1 -1
  54. package/src/validate-mixin.js +1 -1
  55. package/src/virtual-keyboard-controller.d.ts +14 -0
  56. package/src/virtual-keyboard-controller.js +38 -0
  57. package/src/slot-controller.d.ts +0 -8
  58. package/src/slot-controller.js +0 -36
package/LICENSE CHANGED
@@ -175,7 +175,7 @@
175
175
 
176
176
  END OF TERMS AND CONDITIONS
177
177
 
178
- Copyright 2021 Vaadin Ltd.
178
+ Copyright 2021 - 2022 Vaadin Ltd..
179
179
 
180
180
  Licensed under the Apache License, Version 2.0 (the "License");
181
181
  you may not use this file except in compliance with the License.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/field-base",
3
- "version": "22.0.1",
3
+ "version": "23.0.0-alpha3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -31,7 +31,7 @@
31
31
  "dependencies": {
32
32
  "@open-wc/dedupe-mixin": "^1.3.0",
33
33
  "@polymer/polymer": "^3.0.0",
34
- "@vaadin/component-base": "^22.0.1",
34
+ "@vaadin/component-base": "23.0.0-alpha3",
35
35
  "lit": "^2.0.0"
36
36
  },
37
37
  "devDependencies": {
@@ -39,5 +39,5 @@
39
39
  "@vaadin/testing-helpers": "^0.3.2",
40
40
  "sinon": "^9.2.1"
41
41
  },
42
- "gitHead": "2b0a2bff0369d6020f7cc33ad35506aa2d1f6f68"
42
+ "gitHead": "490037919a9e054cc002c1b3be0c94a1603e1a44"
43
43
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { Constructor } from '@open-wc/dedupe-mixin';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { Constructor } from '@open-wc/dedupe-mixin';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { Constructor } from '@open-wc/dedupe-mixin';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
 
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
+ import { addValueToAttribute, removeValueFromAttribute } from './utils.js';
6
7
 
7
8
  /**
8
9
  * A controller for managing ARIA attributes for a field element:
@@ -160,16 +161,12 @@ export class FieldAriaController {
160
161
  return;
161
162
  }
162
163
 
163
- const value = this.__target.getAttribute(attr);
164
- const ids = value ? new Set(value.split(' ')) : new Set();
165
-
166
164
  if (oldId) {
167
- ids.delete(oldId);
165
+ removeValueFromAttribute(this.__target, attr, oldId);
168
166
  }
167
+
169
168
  if (newId) {
170
- ids.add(newId);
169
+ addValueToAttribute(this.__target, attr, newId);
171
170
  }
172
-
173
- this.__target.setAttribute(attr, [...ids].join(' '));
174
171
  }
175
172
  }
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { Constructor } from '@open-wc/dedupe-mixin';
7
7
  import { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js';
8
+ import { SlotMixinClass } from '@vaadin/component-base/src/slot-mixin.js';
8
9
  import { LabelMixinClass } from './label-mixin.js';
9
10
  import { ValidateMixinClass } from './validate-mixin.js';
10
11
 
@@ -17,6 +18,7 @@ export declare function FieldMixin<T extends Constructor<HTMLElement>>(
17
18
  Constructor<ControllerMixinClass> &
18
19
  Constructor<FieldMixinClass> &
19
20
  Constructor<LabelMixinClass> &
21
+ Constructor<SlotMixinClass> &
20
22
  Constructor<ValidateMixinClass>;
21
23
 
22
24
  export declare class FieldMixinClass {
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
3
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
7
- import { animationFrame } from '@vaadin/component-base/src/async.js';
8
6
  import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
9
- import { Debouncer } from '@vaadin/component-base/src/debounce.js';
7
+ import { SlotMixin } from '@vaadin/component-base/src/slot-mixin.js';
10
8
  import { FieldAriaController } from './field-aria-controller.js';
9
+ import { HelperController } from './helper-controller.js';
11
10
  import { LabelMixin } from './label-mixin.js';
12
11
  import { ValidateMixin } from './validate-mixin.js';
13
12
 
@@ -17,10 +16,11 @@ import { ValidateMixin } from './validate-mixin.js';
17
16
  * @polymerMixin
18
17
  * @mixes ControllerMixin
19
18
  * @mixes LabelMixin
19
+ * @mixes SlotMixin
20
20
  * @mixes ValidateMixin
21
21
  */
22
22
  export const FieldMixin = (superclass) =>
23
- class FieldMixinClass extends ValidateMixin(LabelMixin(ControllerMixin(superclass))) {
23
+ class FieldMixinClass extends ValidateMixin(LabelMixin(ControllerMixin(SlotMixin(superclass)))) {
24
24
  static get properties() {
25
25
  return {
26
26
  /**
@@ -68,13 +68,7 @@ export const FieldMixin = (superclass) =>
68
68
  }
69
69
 
70
70
  static get observers() {
71
- return [
72
- '__observeOffsetHeight(errorMessage, invalid, label, helperText)',
73
- '_updateErrorMessage(invalid, errorMessage)',
74
- '_invalidChanged(invalid)',
75
- '_requiredChanged(required)',
76
- '_helperIdChanged(_helperId)'
77
- ];
71
+ return ['_updateErrorMessage(invalid, errorMessage)', '_invalidChanged(invalid)', '_requiredChanged(required)'];
78
72
  }
79
73
 
80
74
  /**
@@ -85,12 +79,17 @@ export const FieldMixin = (superclass) =>
85
79
  return this._getDirectSlotChild('error-message');
86
80
  }
87
81
 
82
+ /** @protected */
83
+ get _helperId() {
84
+ return this._helperController.helperId;
85
+ }
86
+
88
87
  /**
89
88
  * @protected
90
89
  * @return {HTMLElement}
91
90
  */
92
91
  get _helperNode() {
93
- return this._getDirectSlotChild('helper');
92
+ return this._helperController.node;
94
93
  }
95
94
 
96
95
  constructor() {
@@ -99,12 +98,22 @@ export const FieldMixin = (superclass) =>
99
98
  // Ensure every instance has unique ID
100
99
  const uniqueId = (FieldMixinClass._uniqueFieldId = 1 + FieldMixinClass._uniqueFieldId || 0);
101
100
  this._errorId = `error-${this.localName}-${uniqueId}`;
102
- this._helperId = `helper-${this.localName}-${uniqueId}`;
103
-
104
- // Save generated ID to restore later
105
- this.__savedHelperId = this._helperId;
106
101
 
107
102
  this._fieldAriaController = new FieldAriaController(this);
103
+ this._helperController = new HelperController(this);
104
+
105
+ this.addController(this._fieldAriaController);
106
+ this.addController(this._helperController);
107
+
108
+ this._labelController.addEventListener('label-changed', (event) => {
109
+ const { hasLabel, node } = event.detail;
110
+ this.__labelChanged(hasLabel, node);
111
+ });
112
+
113
+ this._helperController.addEventListener('helper-changed', (event) => {
114
+ const { hasHelper, node } = event.detail;
115
+ this.__helperChanged(hasHelper, node);
116
+ });
108
117
  }
109
118
 
110
119
  /** @protected */
@@ -119,54 +128,6 @@ export const FieldMixin = (superclass) =>
119
128
 
120
129
  this._updateErrorMessage(this.invalid, this.errorMessage);
121
130
  }
122
-
123
- const helper = this._helperNode;
124
- if (helper) {
125
- this.__applyCustomHelper(helper);
126
- }
127
-
128
- this.__helperSlot = this.shadowRoot.querySelector('[name="helper"]');
129
-
130
- this.__helperSlotObserver = new FlattenedNodesObserver(this.__helperSlot, (info) => {
131
- const helper = this._currentHelper;
132
-
133
- const newHelper = info.addedNodes.find((node) => node !== helper);
134
- const oldHelper = info.removedNodes.find((node) => node === helper);
135
-
136
- if (newHelper) {
137
- // Custom helper is added, remove the previous one.
138
- if (helper && helper.isConnected) {
139
- this.removeChild(helper);
140
- }
141
-
142
- this.__applyCustomHelper(newHelper);
143
-
144
- this.__helperIdObserver = new MutationObserver((mutations) => {
145
- mutations.forEach((mutation) => {
146
- // only handle helper nodes
147
- if (
148
- mutation.type === 'attributes' &&
149
- mutation.attributeName === 'id' &&
150
- mutation.target === this._currentHelper &&
151
- mutation.target.id !== this.__savedHelperId
152
- ) {
153
- this.__updateHelperId(mutation.target);
154
- }
155
- });
156
- });
157
-
158
- this.__helperIdObserver.observe(newHelper, { attributes: true });
159
- } else if (oldHelper) {
160
- // The observer does not exist when default helper is removed.
161
- if (this.__helperIdObserver) {
162
- this.__helperIdObserver.disconnect();
163
- }
164
-
165
- this.__applyDefaultHelper(this.helperText);
166
- }
167
- });
168
-
169
- this.addController(this._fieldAriaController);
170
131
  }
171
132
 
172
133
  /** @private */
@@ -178,107 +139,21 @@ export const FieldMixin = (superclass) =>
178
139
  }
179
140
  }
180
141
 
181
- /**
182
- * @param {HTMLElement} helper
183
- * @private
184
- */
185
- __applyCustomHelper(helper) {
186
- this.__updateHelperId(helper);
187
- this._currentHelper = helper;
188
- this.__toggleHasHelper(helper.children.length > 0 || this.__isNotEmpty(helper.textContent));
189
- }
190
-
191
- /**
192
- * @param {string} helperText
193
- * @private
194
- */
195
- __isNotEmpty(helperText) {
196
- return helperText && helperText.trim() !== '';
197
- }
198
-
199
142
  /** @private */
200
- __attachDefaultHelper() {
201
- let helper = this.__defaultHelper;
202
-
203
- if (!helper) {
204
- helper = document.createElement('div');
205
- helper.setAttribute('slot', 'helper');
206
- this.__defaultHelper = helper;
207
- }
208
-
209
- helper.id = this.__savedHelperId;
210
- this._helperId = helper.id;
211
- this.appendChild(helper);
212
- this._currentHelper = helper;
213
-
214
- return helper;
215
- }
216
-
217
- /**
218
- * @param {string} helperText
219
- * @private
220
- */
221
- __applyDefaultHelper(helperText) {
222
- let helper = this._helperNode;
223
-
224
- const hasHelperText = this.__isNotEmpty(helperText);
225
- if (hasHelperText && !helper) {
226
- // Create helper lazily
227
- helper = this.__attachDefaultHelper();
228
- }
229
-
230
- // Only set text content for default helper
231
- if (helper && helper === this.__defaultHelper) {
232
- helper.textContent = helperText;
233
- }
234
-
235
- this.__toggleHasHelper(hasHelperText);
236
- }
237
-
238
- /**
239
- * @param {boolean} hasHelper
240
- * @private
241
- */
242
- __toggleHasHelper(hasHelper) {
243
- this.toggleAttribute('has-helper', hasHelper);
244
- }
245
-
246
- /**
247
- * Dispatch an event if a specific size measurement property has changed.
248
- * Supporting multiple properties here is needed for `vaadin-text-area`.
249
- * @protected
250
- */
251
- _dispatchIronResizeEventIfNeeded(prop, value) {
252
- const oldSize = '__old' + prop;
253
- if (this[oldSize] !== undefined && this[oldSize] !== value) {
254
- this.dispatchEvent(new CustomEvent('iron-resize', { bubbles: true, composed: true }));
143
+ __helperChanged(hasHelper, helperNode) {
144
+ if (hasHelper) {
145
+ this._fieldAriaController.setHelperId(helperNode.id);
146
+ } else {
147
+ this._fieldAriaController.setHelperId(null);
255
148
  }
256
-
257
- this[oldSize] = value;
258
149
  }
259
150
 
260
151
  /** @private */
261
- __observeOffsetHeight() {
262
- this.__observeOffsetHeightDebouncer = Debouncer.debounce(
263
- this.__observeOffsetHeightDebouncer,
264
- animationFrame,
265
- () => {
266
- this._dispatchIronResizeEventIfNeeded('Height', this.offsetHeight);
267
- }
268
- );
269
- }
270
-
271
- /**
272
- * @protected
273
- * @override
274
- */
275
- _toggleHasLabelAttribute() {
276
- super._toggleHasLabelAttribute();
277
-
152
+ __labelChanged(hasLabel, labelNode) {
278
153
  // Label ID should be only added when the label content is present.
279
154
  // Otherwise, it may conflict with an `aria-label` attribute possibly added by the user.
280
- if (this.hasAttribute('has-label')) {
281
- this._fieldAriaController.setLabelId(this._labelId);
155
+ if (hasLabel) {
156
+ this._fieldAriaController.setLabelId(labelNode.id);
282
157
  } else {
283
158
  this._fieldAriaController.setLabelId(null);
284
159
  }
@@ -313,29 +188,12 @@ export const FieldMixin = (superclass) =>
313
188
  }
314
189
  }
315
190
 
316
- /**
317
- * @param {HTMLElement} customHelper
318
- * @private
319
- */
320
- __updateHelperId(customHelper) {
321
- let newId;
322
-
323
- if (customHelper.id) {
324
- newId = customHelper.id;
325
- } else {
326
- newId = this.__savedHelperId;
327
- customHelper.id = newId;
328
- }
329
-
330
- this._helperId = newId;
331
- }
332
-
333
191
  /**
334
192
  * @param {string} helperText
335
193
  * @protected
336
194
  */
337
195
  _helperTextChanged(helperText) {
338
- this.__applyDefaultHelper(helperText);
196
+ this._helperController.setHelperText(helperText);
339
197
  }
340
198
 
341
199
  /**
@@ -356,25 +214,23 @@ export const FieldMixin = (superclass) =>
356
214
  this._fieldAriaController.setRequired(required);
357
215
  }
358
216
 
359
- /**
360
- * @param {string} helperId
361
- * @protected
362
- */
363
- _helperIdChanged(helperId) {
364
- this._fieldAriaController.setHelperId(helperId);
365
- }
366
-
367
217
  /**
368
218
  * @param {boolean} required
369
219
  * @protected
370
220
  */
371
221
  _invalidChanged(invalid) {
372
- // Error message ID needs to be dynamically added / removed based on the validity
373
- // Otherwise assistive technologies would announce the error, even if we hide it.
374
- if (invalid) {
375
- this._fieldAriaController.setErrorId(this._errorId);
376
- } else {
377
- this._fieldAriaController.setErrorId(null);
378
- }
222
+ // This timeout is needed to prevent NVDA from announcing the error message twice:
223
+ // 1. Once adding the `[role=alert]` attribute by the `_updateErrorMessage` method (OK).
224
+ // 2. Once linking the error ID with the ARIA target here (unwanted).
225
+ // Related issue: https://github.com/vaadin/web-components/issues/3061.
226
+ setTimeout(() => {
227
+ // Error message ID needs to be dynamically added / removed based on the validity
228
+ // Otherwise assistive technologies would announce the error, even if we hide it.
229
+ if (invalid) {
230
+ this._fieldAriaController.setErrorId(this._errorId);
231
+ } else {
232
+ this._fieldAriaController.setErrorId(null);
233
+ }
234
+ });
379
235
  }
380
236
  };
@@ -0,0 +1,23 @@
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 that manages the helper node content.
10
+ */
11
+ export class HelperController extends SlotController {
12
+ /**
13
+ * String used for the helper text.
14
+ */
15
+ helperText: string | null | undefined;
16
+
17
+ helperId: string;
18
+
19
+ /**
20
+ * Set helper text based on corresponding host property.
21
+ */
22
+ setHelperText(helperText: string): void;
23
+ }
@@ -0,0 +1,184 @@
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 that manages the helper node content.
10
+ */
11
+ export class HelperController extends SlotController {
12
+ constructor(host) {
13
+ // Do not provide slot factory, as only create helper lazily.
14
+ super(host, 'helper');
15
+ }
16
+
17
+ get helperId() {
18
+ return this.node && this.node.id;
19
+ }
20
+
21
+ /**
22
+ * Override to initialize the newly added custom helper.
23
+ *
24
+ * @param {Node} helperNode
25
+ * @protected
26
+ * @override
27
+ */
28
+ initCustomNode(helperNode) {
29
+ this.__updateHelperId(helperNode);
30
+
31
+ this.__observeHelper(helperNode);
32
+
33
+ const hasHelper = this.__hasHelper(helperNode);
34
+ this.__toggleHasHelper(hasHelper);
35
+ }
36
+
37
+ /**
38
+ * Override to cleanup helper node when it's removed.
39
+ *
40
+ * @param {Node} _node
41
+ * @protected
42
+ * @override
43
+ */
44
+ teardownNode(_node) {
45
+ // The observer does not exist when the default helper is removed.
46
+ if (this.__helperIdObserver) {
47
+ this.__helperIdObserver.disconnect();
48
+ }
49
+
50
+ const helperNode = this.getSlotChild();
51
+
52
+ // Custom node is added to helper slot
53
+ if (helperNode && helperNode !== this.defaultNode) {
54
+ const hasHelper = this.__hasHelper(helperNode);
55
+ this.__toggleHasHelper(hasHelper);
56
+ } else {
57
+ // Restore default helper if needed
58
+ this.__applyDefaultHelper(this.helperText, helperNode);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Set helper text based on corresponding host property.
64
+ * @param {string} helperText
65
+ */
66
+ setHelperText(helperText) {
67
+ this.helperText = helperText;
68
+
69
+ if (!this.node || this.node === this.defaultNode) {
70
+ this.__applyDefaultHelper(helperText, this.defaultNode);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * @param {HTMLElement} helperNode
76
+ * @return {boolean}
77
+ * @private
78
+ */
79
+ __hasHelper(helperNode) {
80
+ if (!helperNode) {
81
+ return false;
82
+ }
83
+
84
+ return helperNode.children.length > 0 || this.__isNotEmpty(helperNode.textContent);
85
+ }
86
+
87
+ /**
88
+ * @param {string} helperText
89
+ * @private
90
+ */
91
+ __isNotEmpty(helperText) {
92
+ return helperText && helperText.trim() !== '';
93
+ }
94
+
95
+ /**
96
+ * @param {string} helperText
97
+ * @param {Node} helperNode
98
+ * @private
99
+ */
100
+ __applyDefaultHelper(helperText, helperNode) {
101
+ const hasHelperText = this.__isNotEmpty(helperText);
102
+
103
+ if (hasHelperText && !helperNode) {
104
+ // Set slot factory lazily to only create helper node when needed.
105
+ this.slotFactory = () => document.createElement('div');
106
+
107
+ helperNode = this.attachDefaultNode();
108
+
109
+ this.__updateHelperId(helperNode);
110
+ this.__observeHelper(helperNode);
111
+ }
112
+
113
+ if (helperNode) {
114
+ helperNode.textContent = helperText;
115
+ }
116
+
117
+ this.__toggleHasHelper(hasHelperText);
118
+ }
119
+
120
+ /**
121
+ * @param {HTMLElement} helperNode
122
+ * @private
123
+ */
124
+ __observeHelper(helperNode) {
125
+ this.__helperObserver = new MutationObserver((mutations) => {
126
+ mutations.forEach((mutation) => {
127
+ const target = mutation.target;
128
+
129
+ // Ensure the mutation target is the currently connected helper
130
+ // to ignore async mutations dispatched for removed element.
131
+ const isHelperMutation = target === this.node;
132
+
133
+ if (mutation.type === 'attributes') {
134
+ // We use attributeFilter to only observe ID mutation,
135
+ // no need to check for attribute name separately.
136
+ if (isHelperMutation && target.id !== this.defaultId) {
137
+ this.__updateHelperId(target);
138
+ }
139
+ } else if (isHelperMutation || target.parentElement === this.node) {
140
+ // Update has-helper when textContent changes
141
+ const hasHelper = this.__hasHelper(this.node);
142
+ this.__toggleHasHelper(hasHelper);
143
+ }
144
+ });
145
+ });
146
+
147
+ // Observe changes to helper ID attribute, text content and children.
148
+ this.__helperObserver.observe(helperNode, {
149
+ attributes: true,
150
+ attributeFilter: ['id'],
151
+ childList: true,
152
+ subtree: true,
153
+ characterData: true
154
+ });
155
+ }
156
+
157
+ /**
158
+ * @param {boolean} hasHelper
159
+ * @private
160
+ */
161
+ __toggleHasHelper(hasHelper) {
162
+ this.host.toggleAttribute('has-helper', hasHelper);
163
+
164
+ // Make it possible for other mixins to observe change
165
+ this.dispatchEvent(
166
+ new CustomEvent('helper-changed', {
167
+ detail: {
168
+ hasHelper,
169
+ node: this.node
170
+ }
171
+ })
172
+ );
173
+ }
174
+
175
+ /**
176
+ * @param {HTMLElement} helperNode
177
+ * @private
178
+ */
179
+ __updateHelperId(helperNode) {
180
+ if (!helperNode.id) {
181
+ helperNode.id = this.defaultId;
182
+ }
183
+ }
184
+ }