@vaadin/field-base 22.0.0 → 22.0.4

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 (56) 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 +1 -1
  11. package/src/field-mixin.d.ts +3 -1
  12. package/src/field-mixin.js +50 -161
  13. package/src/helper-controller.d.ts +23 -0
  14. package/src/helper-controller.js +185 -0
  15. package/src/input-constraints-mixin.d.ts +1 -1
  16. package/src/input-constraints-mixin.js +1 -1
  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 +16 -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 -1
  33. package/src/shadow-focus-mixin.d.ts +1 -1
  34. package/src/shadow-focus-mixin.js +1 -1
  35. package/src/slot-styles-mixin.d.ts +1 -1
  36. package/src/slot-styles-mixin.js +1 -1
  37. package/src/slot-target-controller.d.ts +31 -0
  38. package/src/slot-target-controller.js +119 -0
  39. package/src/styles/clear-button-styles.d.ts +1 -1
  40. package/src/styles/clear-button-styles.js +1 -1
  41. package/src/styles/field-shared-styles.d.ts +1 -1
  42. package/src/styles/field-shared-styles.js +1 -1
  43. package/src/styles/input-field-container-styles.d.ts +1 -1
  44. package/src/styles/input-field-container-styles.js +1 -1
  45. package/src/styles/input-field-shared-styles.d.ts +1 -1
  46. package/src/styles/input-field-shared-styles.js +1 -1
  47. package/src/text-area-controller.d.ts +3 -3
  48. package/src/text-area-controller.js +5 -4
  49. package/src/validate-mixin.d.ts +1 -1
  50. package/src/validate-mixin.js +1 -1
  51. package/src/slot-controller.d.ts +0 -8
  52. package/src/slot-controller.js +0 -36
  53. package/src/slot-label-mixin.d.ts +0 -15
  54. package/src/slot-label-mixin.js +0 -38
  55. package/src/slot-target-mixin.d.ts +0 -25
  56. package/src/slot-target-mixin.js +0 -110
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.0",
3
+ "version": "22.0.4",
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.0",
34
+ "@vaadin/component-base": "^22.0.4",
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": "b668e9b1a975227fbe34beb70d1cd5b03dce2348"
42
+ "gitHead": "55891f68d4da41e846e06dfe51dceba1665e41ce"
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,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,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,14 @@
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
6
  import { animationFrame } from '@vaadin/component-base/src/async.js';
8
7
  import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
9
8
  import { Debouncer } from '@vaadin/component-base/src/debounce.js';
9
+ import { SlotMixin } from '@vaadin/component-base/src/slot-mixin.js';
10
10
  import { FieldAriaController } from './field-aria-controller.js';
11
+ import { HelperController } from './helper-controller.js';
11
12
  import { LabelMixin } from './label-mixin.js';
12
13
  import { ValidateMixin } from './validate-mixin.js';
13
14
 
@@ -17,10 +18,11 @@ import { ValidateMixin } from './validate-mixin.js';
17
18
  * @polymerMixin
18
19
  * @mixes ControllerMixin
19
20
  * @mixes LabelMixin
21
+ * @mixes SlotMixin
20
22
  * @mixes ValidateMixin
21
23
  */
22
24
  export const FieldMixin = (superclass) =>
23
- class FieldMixinClass extends ValidateMixin(LabelMixin(ControllerMixin(superclass))) {
25
+ class FieldMixinClass extends ValidateMixin(LabelMixin(ControllerMixin(SlotMixin(superclass)))) {
24
26
  static get properties() {
25
27
  return {
26
28
  /**
@@ -72,8 +74,7 @@ export const FieldMixin = (superclass) =>
72
74
  '__observeOffsetHeight(errorMessage, invalid, label, helperText)',
73
75
  '_updateErrorMessage(invalid, errorMessage)',
74
76
  '_invalidChanged(invalid)',
75
- '_requiredChanged(required)',
76
- '_helperIdChanged(_helperId)'
77
+ '_requiredChanged(required)'
77
78
  ];
78
79
  }
79
80
 
@@ -85,12 +86,17 @@ export const FieldMixin = (superclass) =>
85
86
  return this._getDirectSlotChild('error-message');
86
87
  }
87
88
 
89
+ /** @protected */
90
+ get _helperId() {
91
+ return this._helperController.helperId;
92
+ }
93
+
88
94
  /**
89
95
  * @protected
90
96
  * @return {HTMLElement}
91
97
  */
92
98
  get _helperNode() {
93
- return this._getDirectSlotChild('helper');
99
+ return this._helperController.node;
94
100
  }
95
101
 
96
102
  constructor() {
@@ -99,12 +105,22 @@ export const FieldMixin = (superclass) =>
99
105
  // Ensure every instance has unique ID
100
106
  const uniqueId = (FieldMixinClass._uniqueFieldId = 1 + FieldMixinClass._uniqueFieldId || 0);
101
107
  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
108
 
107
109
  this._fieldAriaController = new FieldAriaController(this);
110
+ this._helperController = new HelperController(this);
111
+
112
+ this.addController(this._fieldAriaController);
113
+ this.addController(this._helperController);
114
+
115
+ this._labelController.addEventListener('label-changed', (event) => {
116
+ const { hasLabel, node } = event.detail;
117
+ this.__labelChanged(hasLabel, node);
118
+ });
119
+
120
+ this._helperController.addEventListener('helper-changed', (event) => {
121
+ const { hasHelper, node } = event.detail;
122
+ this.__helperChanged(hasHelper, node);
123
+ });
108
124
  }
109
125
 
110
126
  /** @protected */
@@ -119,54 +135,6 @@ export const FieldMixin = (superclass) =>
119
135
 
120
136
  this._updateErrorMessage(this.invalid, this.errorMessage);
121
137
  }
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
138
  }
171
139
 
172
140
  /** @private */
@@ -178,69 +146,13 @@ export const FieldMixin = (superclass) =>
178
146
  }
179
147
  }
180
148
 
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
149
  /** @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;
150
+ __helperChanged(hasHelper, helperNode) {
151
+ if (hasHelper) {
152
+ this._fieldAriaController.setHelperId(helperNode.id);
153
+ } else {
154
+ this._fieldAriaController.setHelperId(null);
233
155
  }
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
156
  }
245
157
 
246
158
  /**
@@ -268,17 +180,12 @@ export const FieldMixin = (superclass) =>
268
180
  );
269
181
  }
270
182
 
271
- /**
272
- * @protected
273
- * @override
274
- */
275
- _toggleHasLabelAttribute() {
276
- super._toggleHasLabelAttribute();
277
-
183
+ /** @private */
184
+ __labelChanged(hasLabel, labelNode) {
278
185
  // Label ID should be only added when the label content is present.
279
186
  // 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);
187
+ if (hasLabel) {
188
+ this._fieldAriaController.setLabelId(labelNode.id);
282
189
  } else {
283
190
  this._fieldAriaController.setLabelId(null);
284
191
  }
@@ -301,6 +208,7 @@ export const FieldMixin = (superclass) =>
301
208
  }
302
209
  const hasError = Boolean(invalid && errorMessage);
303
210
  error.textContent = hasError ? errorMessage : '';
211
+ error.hidden = !hasError;
304
212
  this.toggleAttribute('has-error-message', hasError);
305
213
 
306
214
  // Role alert will make the error message announce immediately
@@ -312,29 +220,12 @@ export const FieldMixin = (superclass) =>
312
220
  }
313
221
  }
314
222
 
315
- /**
316
- * @param {HTMLElement} customHelper
317
- * @private
318
- */
319
- __updateHelperId(customHelper) {
320
- let newId;
321
-
322
- if (customHelper.id) {
323
- newId = customHelper.id;
324
- } else {
325
- newId = this.__savedHelperId;
326
- customHelper.id = newId;
327
- }
328
-
329
- this._helperId = newId;
330
- }
331
-
332
223
  /**
333
224
  * @param {string} helperText
334
225
  * @protected
335
226
  */
336
227
  _helperTextChanged(helperText) {
337
- this.__applyDefaultHelper(helperText);
228
+ this._helperController.setHelperText(helperText);
338
229
  }
339
230
 
340
231
  /**
@@ -355,25 +246,23 @@ export const FieldMixin = (superclass) =>
355
246
  this._fieldAriaController.setRequired(required);
356
247
  }
357
248
 
358
- /**
359
- * @param {string} helperId
360
- * @protected
361
- */
362
- _helperIdChanged(helperId) {
363
- this._fieldAriaController.setHelperId(helperId);
364
- }
365
-
366
249
  /**
367
250
  * @param {boolean} required
368
251
  * @protected
369
252
  */
370
253
  _invalidChanged(invalid) {
371
- // Error message ID needs to be dynamically added / removed based on the validity
372
- // Otherwise assistive technologies would announce the error, even if we hide it.
373
- if (invalid) {
374
- this._fieldAriaController.setErrorId(this._errorId);
375
- } else {
376
- this._fieldAriaController.setErrorId(null);
377
- }
254
+ // This timeout is needed to prevent NVDA from announcing the error message twice:
255
+ // 1. Once adding the `[role=alert]` attribute by the `_updateErrorMessage` method (OK).
256
+ // 2. Once linking the error ID with the ARIA target here (unwanted).
257
+ // Related issue: https://github.com/vaadin/web-components/issues/3061.
258
+ setTimeout(() => {
259
+ // Error message ID needs to be dynamically added / removed based on the validity
260
+ // Otherwise assistive technologies would announce the error, even if we hide it.
261
+ if (invalid) {
262
+ this._fieldAriaController.setErrorId(this._errorId);
263
+ } else {
264
+ this._fieldAriaController.setErrorId(null);
265
+ }
266
+ });
378
267
  }
379
268
  };
@@ -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,185 @@
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
+ const helperNode = this.getSlotChild();
70
+ if (!helperNode || helperNode === this.defaultNode) {
71
+ this.__applyDefaultHelper(helperText, helperNode);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * @param {HTMLElement} helperNode
77
+ * @return {boolean}
78
+ * @private
79
+ */
80
+ __hasHelper(helperNode) {
81
+ if (!helperNode) {
82
+ return false;
83
+ }
84
+
85
+ return helperNode.children.length > 0 || this.__isNotEmpty(helperNode.textContent);
86
+ }
87
+
88
+ /**
89
+ * @param {string} helperText
90
+ * @private
91
+ */
92
+ __isNotEmpty(helperText) {
93
+ return helperText && helperText.trim() !== '';
94
+ }
95
+
96
+ /**
97
+ * @param {string} helperText
98
+ * @param {Node} helperNode
99
+ * @private
100
+ */
101
+ __applyDefaultHelper(helperText, helperNode) {
102
+ const hasHelperText = this.__isNotEmpty(helperText);
103
+
104
+ if (hasHelperText && !helperNode) {
105
+ // Set slot factory lazily to only create helper node when needed.
106
+ this.slotFactory = () => document.createElement('div');
107
+
108
+ helperNode = this.attachDefaultNode();
109
+
110
+ this.__updateHelperId(helperNode);
111
+ this.__observeHelper(helperNode);
112
+ }
113
+
114
+ if (helperNode) {
115
+ helperNode.textContent = helperText;
116
+ }
117
+
118
+ this.__toggleHasHelper(hasHelperText);
119
+ }
120
+
121
+ /**
122
+ * @param {HTMLElement} helperNode
123
+ * @private
124
+ */
125
+ __observeHelper(helperNode) {
126
+ this.__helperObserver = new MutationObserver((mutations) => {
127
+ mutations.forEach((mutation) => {
128
+ const target = mutation.target;
129
+
130
+ // Ensure the mutation target is the currently connected helper
131
+ // to ignore async mutations dispatched for removed element.
132
+ const isHelperMutation = target === this.node;
133
+
134
+ if (mutation.type === 'attributes') {
135
+ // We use attributeFilter to only observe ID mutation,
136
+ // no need to check for attribute name separately.
137
+ if (isHelperMutation && target.id !== this.defaultId) {
138
+ this.__updateHelperId(target);
139
+ }
140
+ } else if (isHelperMutation || target.parentElement === this.node) {
141
+ // Update has-helper when textContent changes
142
+ const hasHelper = this.__hasHelper(this.node);
143
+ this.__toggleHasHelper(hasHelper);
144
+ }
145
+ });
146
+ });
147
+
148
+ // Observe changes to helper ID attribute, text content and children.
149
+ this.__helperObserver.observe(helperNode, {
150
+ attributes: true,
151
+ attributeFilter: ['id'],
152
+ childList: true,
153
+ subtree: true,
154
+ characterData: true
155
+ });
156
+ }
157
+
158
+ /**
159
+ * @param {boolean} hasHelper
160
+ * @private
161
+ */
162
+ __toggleHasHelper(hasHelper) {
163
+ this.host.toggleAttribute('has-helper', hasHelper);
164
+
165
+ // Make it possible for other mixins to observe change
166
+ this.dispatchEvent(
167
+ new CustomEvent('helper-changed', {
168
+ detail: {
169
+ hasHelper,
170
+ node: this.node
171
+ }
172
+ })
173
+ );
174
+ }
175
+
176
+ /**
177
+ * @param {HTMLElement} helperNode
178
+ * @private
179
+ */
180
+ __updateHelperId(helperNode) {
181
+ if (!helperNode.id) {
182
+ helperNode.id = this.defaultId;
183
+ }
184
+ }
185
+ }
@@ -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 { KeyboardMixin } from '@vaadin/component-base/src/keyboard-mixin.js';