@vaadin/field-base 22.0.0-alpha3 → 22.0.0-alpha7

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 (77) hide show
  1. package/index.d.ts +9 -14
  2. package/index.js +9 -14
  3. package/package.json +24 -18
  4. package/src/aria-label-controller.d.ts +11 -0
  5. package/src/aria-label-controller.js +53 -0
  6. package/src/checked-mixin.d.ts +21 -0
  7. package/src/checked-mixin.js +54 -0
  8. package/src/delegate-focus-mixin.d.ts +2 -2
  9. package/src/delegate-focus-mixin.js +150 -133
  10. package/src/delegate-state-mixin.d.ts +23 -0
  11. package/src/delegate-state-mixin.js +125 -0
  12. package/src/field-mixin.d.ts +39 -0
  13. package/src/field-mixin.js +317 -0
  14. package/src/input-constraints-mixin.d.ts +2 -1
  15. package/src/input-constraints-mixin.js +105 -53
  16. package/src/input-control-mixin.d.ts +52 -0
  17. package/src/input-control-mixin.js +170 -0
  18. package/src/input-controller.d.ts +11 -0
  19. package/src/input-controller.js +35 -0
  20. package/src/input-field-mixin.d.ts +2 -16
  21. package/src/input-field-mixin.js +20 -97
  22. package/src/input-mixin.d.ts +1 -1
  23. package/src/input-mixin.js +156 -145
  24. package/src/label-mixin.d.ts +2 -2
  25. package/src/label-mixin.js +73 -60
  26. package/src/pattern-mixin.d.ts +2 -2
  27. package/src/pattern-mixin.js +13 -13
  28. package/src/shadow-focus-mixin.d.ts +21 -0
  29. package/src/shadow-focus-mixin.js +87 -0
  30. package/src/slot-controller.d.ts +8 -0
  31. package/src/slot-controller.js +36 -0
  32. package/src/slot-label-mixin.d.ts +20 -0
  33. package/src/slot-label-mixin.js +38 -0
  34. package/src/slot-styles-mixin.d.ts +24 -0
  35. package/src/slot-styles-mixin.js +76 -0
  36. package/src/slot-target-mixin.d.ts +32 -0
  37. package/src/slot-target-mixin.js +110 -0
  38. package/src/styles/clear-button-styles.d.ts +8 -0
  39. package/src/styles/clear-button-styles.js +21 -0
  40. package/src/styles/field-shared-styles.d.ts +8 -0
  41. package/src/styles/field-shared-styles.js +29 -0
  42. package/src/styles/input-field-container-styles.d.ts +8 -0
  43. package/src/styles/input-field-container-styles.js +16 -0
  44. package/src/styles/input-field-shared-styles.d.ts +8 -0
  45. package/src/styles/input-field-shared-styles.js +10 -0
  46. package/src/text-area-controller.d.ts +11 -0
  47. package/src/text-area-controller.js +38 -0
  48. package/src/validate-mixin.d.ts +1 -9
  49. package/src/validate-mixin.js +43 -118
  50. package/src/active-mixin.d.ts +0 -25
  51. package/src/active-mixin.js +0 -84
  52. package/src/aria-label-mixin.d.ts +0 -20
  53. package/src/aria-label-mixin.js +0 -71
  54. package/src/char-length-mixin.d.ts +0 -30
  55. package/src/char-length-mixin.js +0 -42
  56. package/src/clear-button-mixin.d.ts +0 -27
  57. package/src/clear-button-mixin.js +0 -80
  58. package/src/disabled-mixin.d.ts +0 -23
  59. package/src/disabled-mixin.js +0 -48
  60. package/src/field-aria-mixin.d.ts +0 -24
  61. package/src/field-aria-mixin.js +0 -61
  62. package/src/focus-mixin.d.ts +0 -33
  63. package/src/focus-mixin.js +0 -104
  64. package/src/forward-input-props-mixin.d.ts +0 -41
  65. package/src/forward-input-props-mixin.js +0 -110
  66. package/src/helper-text-mixin.d.ts +0 -24
  67. package/src/helper-text-mixin.js +0 -109
  68. package/src/input-slot-mixin.d.ts +0 -26
  69. package/src/input-slot-mixin.js +0 -71
  70. package/src/slot-mixin.d.ts +0 -23
  71. package/src/slot-mixin.js +0 -55
  72. package/src/tabindex-mixin.d.ts +0 -29
  73. package/src/tabindex-mixin.js +0 -78
  74. package/src/text-area-slot-mixin.d.ts +0 -21
  75. package/src/text-area-slot-mixin.js +0 -56
  76. package/src/text-field-mixin.d.ts +0 -21
  77. package/src/text-field-mixin.js +0 -17
@@ -0,0 +1,125 @@
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 { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js';
7
+
8
+ /**
9
+ * A mixin to delegate properties and attributes to a target element.
10
+ *
11
+ * @polymerMixin
12
+ */
13
+ export const DelegateStateMixin = dedupingMixin(
14
+ (superclass) =>
15
+ class DelegateStateMixinClass extends superclass {
16
+ static get properties() {
17
+ return {
18
+ /**
19
+ * A target element to which attributes and properties are delegated.
20
+ * @protected
21
+ */
22
+ stateTarget: {
23
+ type: Object,
24
+ observer: '_stateTargetChanged'
25
+ }
26
+ };
27
+ }
28
+
29
+ /**
30
+ * An array of the host attributes to delegate to the target element.
31
+ */
32
+ static get delegateAttrs() {
33
+ return [];
34
+ }
35
+
36
+ /**
37
+ * An array of the host properties to delegate to the target element.
38
+ */
39
+ static get delegateProps() {
40
+ return [];
41
+ }
42
+
43
+ /** @protected */
44
+ ready() {
45
+ super.ready();
46
+
47
+ this._createDelegateAttrsObserver();
48
+ this._createDelegatePropsObserver();
49
+ }
50
+
51
+ /** @protected */
52
+ _stateTargetChanged(target) {
53
+ if (target) {
54
+ this._ensureAttrsDelegated();
55
+ this._ensurePropsDelegated();
56
+ }
57
+ }
58
+
59
+ /** @protected */
60
+ _createDelegateAttrsObserver() {
61
+ this._createMethodObserver(`_delegateAttrsChanged(${this.constructor.delegateAttrs.join(', ')})`);
62
+ }
63
+
64
+ /** @protected */
65
+ _createDelegatePropsObserver() {
66
+ this._createMethodObserver(`_delegatePropsChanged(${this.constructor.delegateProps.join(', ')})`);
67
+ }
68
+
69
+ /** @protected */
70
+ _ensureAttrsDelegated() {
71
+ this.constructor.delegateAttrs.forEach((name) => {
72
+ this._delegateAttribute(name, this[name]);
73
+ });
74
+ }
75
+
76
+ /** @protected */
77
+ _ensurePropsDelegated() {
78
+ this.constructor.delegateProps.forEach((name) => {
79
+ this._delegateProperty(name, this[name]);
80
+ });
81
+ }
82
+
83
+ /** @protected */
84
+ _delegateAttrsChanged(...values) {
85
+ this.constructor.delegateAttrs.forEach((name, index) => {
86
+ this._delegateAttribute(name, values[index]);
87
+ });
88
+ }
89
+
90
+ /** @protected */
91
+ _delegatePropsChanged(...values) {
92
+ this.constructor.delegateProps.forEach((name, index) => {
93
+ this._delegateProperty(name, values[index]);
94
+ });
95
+ }
96
+
97
+ /** @protected */
98
+ _delegateAttribute(name, value) {
99
+ if (!this.stateTarget) {
100
+ return;
101
+ }
102
+
103
+ if (name === 'invalid') {
104
+ this._delegateAttribute('aria-invalid', value ? 'true' : false);
105
+ }
106
+
107
+ if (typeof value === 'boolean') {
108
+ this.stateTarget.toggleAttribute(name, value);
109
+ } else if (value) {
110
+ this.stateTarget.setAttribute(name, value);
111
+ } else {
112
+ this.stateTarget.removeAttribute(name);
113
+ }
114
+ }
115
+
116
+ /** @protected */
117
+ _delegateProperty(name, value) {
118
+ if (!this.stateTarget) {
119
+ return;
120
+ }
121
+
122
+ this.stateTarget[name] = value;
123
+ }
124
+ }
125
+ );
@@ -0,0 +1,39 @@
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 { LabelMixin } from './label-mixin.js';
7
+ import { ValidateMixin } from './validate-mixin.js';
8
+
9
+ /**
10
+ * A mixin to provide common field logic: label, error message and helper text.
11
+ */
12
+ declare function FieldMixin<T extends new (...args: any[]) => {}>(base: T): T & FieldMixinConstructor;
13
+
14
+ interface FieldMixinConstructor {
15
+ new (...args: any[]): FieldMixin;
16
+ }
17
+
18
+ interface FieldMixin extends LabelMixin, ValidateMixin {
19
+ /**
20
+ * A target element to which ARIA attributes are set.
21
+ */
22
+ ariaTarget: HTMLElement;
23
+
24
+ /**
25
+ * String used for the helper text.
26
+ *
27
+ * @attr {string} helper-text
28
+ */
29
+ helperText: string | null | undefined;
30
+
31
+ /**
32
+ * Error to show when the field is invalid.
33
+ *
34
+ * @attr {string} error-message
35
+ */
36
+ errorMessage: string;
37
+ }
38
+
39
+ export { FieldMixin, FieldMixinConstructor };
@@ -0,0 +1,317 @@
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 { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
7
+ import { Debouncer } from '@vaadin/component-base/src/debounce.js';
8
+ import { animationFrame } from '@vaadin/component-base/src/async.js';
9
+ import { LabelMixin } from './label-mixin.js';
10
+ import { ValidateMixin } from './validate-mixin.js';
11
+
12
+ /**
13
+ * A mixin to provide common field logic: label, error message and helper text.
14
+ *
15
+ * @polymerMixin
16
+ * @mixes LabelMixin
17
+ * @mixes ValidateMixin
18
+ */
19
+ export const FieldMixin = (superclass) =>
20
+ class FieldMixinClass extends ValidateMixin(LabelMixin(superclass)) {
21
+ static get properties() {
22
+ return {
23
+ /**
24
+ * A target element to which ARIA attributes are set.
25
+ * @protected
26
+ */
27
+ ariaTarget: {
28
+ type: Object,
29
+ observer: '_ariaTargetChanged'
30
+ },
31
+
32
+ /**
33
+ * Error to show when the field is invalid.
34
+ *
35
+ * @attr {string} error-message
36
+ */
37
+ errorMessage: {
38
+ type: String
39
+ },
40
+
41
+ /**
42
+ * String used for the helper text.
43
+ * @attr {string} helper-text
44
+ */
45
+ helperText: {
46
+ type: String,
47
+ observer: '_helperTextChanged'
48
+ },
49
+
50
+ /** @protected */
51
+ _helperId: String
52
+ };
53
+ }
54
+
55
+ /** @protected */
56
+ get slots() {
57
+ return {
58
+ ...super.slots,
59
+ 'error-message': () => {
60
+ const error = document.createElement('div');
61
+ error.textContent = this.errorMessage;
62
+ error.setAttribute('aria-live', 'assertive');
63
+ return error;
64
+ }
65
+ };
66
+ }
67
+
68
+ static get observers() {
69
+ return [
70
+ '__ariaChanged(invalid, _helperId)',
71
+ '__observeOffsetHeight(errorMessage, invalid, label, helperText)',
72
+ '_updateErrorMessage(invalid, errorMessage)'
73
+ ];
74
+ }
75
+
76
+ /** @protected */
77
+ get _errorNode() {
78
+ return this._getDirectSlotChild('error-message');
79
+ }
80
+
81
+ /** @protected */
82
+ get _helperNode() {
83
+ return this._getDirectSlotChild('helper');
84
+ }
85
+
86
+ /** @protected */
87
+ get _ariaAttr() {
88
+ return 'aria-describedby';
89
+ }
90
+
91
+ constructor() {
92
+ super();
93
+
94
+ // Ensure every instance has unique ID
95
+ const uniqueId = (FieldMixinClass._uniqueFieldId = 1 + FieldMixinClass._uniqueFieldId || 0);
96
+ this._errorId = `error-${this.localName}-${uniqueId}`;
97
+ this._helperId = `helper-${this.localName}-${uniqueId}`;
98
+
99
+ // Save generated ID to restore later
100
+ this.__savedHelperId = this._helperId;
101
+ }
102
+
103
+ /** @protected */
104
+ ready() {
105
+ super.ready();
106
+
107
+ const error = this._errorNode;
108
+ if (error) {
109
+ error.id = this._errorId;
110
+
111
+ this.__applyCustomError();
112
+
113
+ this._updateErrorMessage(this.invalid, this.errorMessage);
114
+ }
115
+
116
+ const helper = this._helperNode;
117
+ if (helper) {
118
+ this.__applyCustomHelper(helper);
119
+ }
120
+
121
+ this.__helperSlot = this.shadowRoot.querySelector('[name="helper"]');
122
+
123
+ this.__helperSlotObserver = new FlattenedNodesObserver(this.__helperSlot, (info) => {
124
+ const helper = this._currentHelper;
125
+
126
+ const newHelper = info.addedNodes.find((node) => node !== helper);
127
+ const oldHelper = info.removedNodes.find((node) => node === helper);
128
+
129
+ if (newHelper) {
130
+ // Custom helper is added, remove the previous one.
131
+ if (helper && helper.isConnected) {
132
+ this.removeChild(helper);
133
+ }
134
+
135
+ this.__applyCustomHelper(newHelper);
136
+
137
+ this.__helperIdObserver = new MutationObserver((mutations) => {
138
+ mutations.forEach((mutation) => {
139
+ // only handle helper nodes
140
+ if (
141
+ mutation.type === 'attributes' &&
142
+ mutation.attributeName === 'id' &&
143
+ mutation.target === this._currentHelper &&
144
+ mutation.target.id !== this.__savedHelperId
145
+ ) {
146
+ this.__updateHelperId(mutation.target);
147
+ }
148
+ });
149
+ });
150
+
151
+ this.__helperIdObserver.observe(newHelper, { attributes: true });
152
+ } else if (oldHelper) {
153
+ // The observer does not exist when default helper is removed.
154
+ if (this.__helperIdObserver) {
155
+ this.__helperIdObserver.disconnect();
156
+ }
157
+
158
+ this.__applyDefaultHelper(this.helperText);
159
+ }
160
+ });
161
+ }
162
+
163
+ /** @private */
164
+ __applyCustomError() {
165
+ const error = this.__errorMessage;
166
+ if (error && error !== this.errorMessage) {
167
+ this.errorMessage = error;
168
+ delete this.__errorMessage;
169
+ }
170
+ }
171
+
172
+ /** @private */
173
+ __applyCustomHelper(helper) {
174
+ this.__updateHelperId(helper);
175
+ this._currentHelper = helper;
176
+ this.__toggleHasHelper(helper.children.length > 0 || this.__isNotEmpty(helper.textContent));
177
+ }
178
+
179
+ /** @private */
180
+ __isNotEmpty(helperText) {
181
+ return helperText && helperText.trim() !== '';
182
+ }
183
+
184
+ /** @private */
185
+ __attachDefaultHelper() {
186
+ let helper = this.__defaultHelper;
187
+
188
+ if (!helper) {
189
+ helper = document.createElement('div');
190
+ helper.setAttribute('slot', 'helper');
191
+ this.__defaultHelper = helper;
192
+ }
193
+
194
+ helper.id = this.__savedHelperId;
195
+ this.appendChild(helper);
196
+ this._currentHelper = helper;
197
+
198
+ return helper;
199
+ }
200
+
201
+ /** @private */
202
+ __applyDefaultHelper(helperText) {
203
+ let helper = this._helperNode;
204
+
205
+ const hasHelperText = this.__isNotEmpty(helperText);
206
+ if (hasHelperText && !helper) {
207
+ // Create helper lazily
208
+ helper = this.__attachDefaultHelper();
209
+ }
210
+
211
+ // Only set text content for default helper
212
+ if (helper && helper === this.__defaultHelper) {
213
+ helper.textContent = helperText;
214
+ }
215
+
216
+ this.__toggleHasHelper(hasHelperText);
217
+ }
218
+
219
+ /** @private */
220
+ __toggleHasHelper(hasHelper) {
221
+ this.toggleAttribute('has-helper', hasHelper);
222
+ }
223
+
224
+ /**
225
+ * Dispatch an event if a specific size measurement property has changed.
226
+ * Supporting multiple properties here is needed for `vaadin-text-area`.
227
+ * @protected
228
+ */
229
+ _dispatchIronResizeEventIfNeeded(prop, value) {
230
+ const oldSize = '__old' + prop;
231
+ if (this[oldSize] !== undefined && this[oldSize] !== value) {
232
+ this.dispatchEvent(new CustomEvent('iron-resize', { bubbles: true, composed: true }));
233
+ }
234
+
235
+ this[oldSize] = value;
236
+ }
237
+
238
+ /** @private */
239
+ __observeOffsetHeight() {
240
+ this.__observeOffsetHeightDebouncer = Debouncer.debounce(
241
+ this.__observeOffsetHeightDebouncer,
242
+ animationFrame,
243
+ () => {
244
+ this._dispatchIronResizeEventIfNeeded('Height', this.offsetHeight);
245
+ }
246
+ );
247
+ }
248
+
249
+ /**
250
+ * @param {boolean} invalid
251
+ * @protected
252
+ */
253
+ _updateErrorMessage(invalid, errorMessage) {
254
+ const error = this._errorNode;
255
+ if (!error) {
256
+ return;
257
+ }
258
+
259
+ // save the custom error message content
260
+ if (error.textContent && !errorMessage) {
261
+ this.__errorMessage = error.textContent.trim();
262
+ }
263
+ const hasError = Boolean(invalid && errorMessage);
264
+ error.textContent = hasError ? errorMessage : '';
265
+ this.toggleAttribute('has-error-message', hasError);
266
+ }
267
+
268
+ /** @private */
269
+ __updateHelperId(customHelper) {
270
+ let newId;
271
+
272
+ if (customHelper.id) {
273
+ newId = customHelper.id;
274
+ } else {
275
+ newId = this.__savedHelperId;
276
+ customHelper.id = newId;
277
+ }
278
+
279
+ this._helperId = newId;
280
+ }
281
+
282
+ /** @protected */
283
+ _helperTextChanged(helperText) {
284
+ this.__applyDefaultHelper(helperText);
285
+ }
286
+
287
+ /** @protected */
288
+ _ariaTargetChanged(target) {
289
+ if (target) {
290
+ this._updateAriaAttribute(this.invalid, this._helperId);
291
+ }
292
+ }
293
+
294
+ /** @protected */
295
+ _updateAriaAttribute(invalid, helperId) {
296
+ const attr = this._ariaAttr;
297
+
298
+ if (this.ariaTarget && attr) {
299
+ // For groups, add all IDs to aria-labelledby rather than aria-describedby -
300
+ // that should guarantee that it's announced when the group is entered.
301
+ const ariaIds = attr === 'aria-describedby' ? [helperId] : [this._labelId, helperId];
302
+
303
+ // Error message ID needs to be dynamically added / removed based on the validity
304
+ // Otherwise assistive technologies would announce the error, even if we hide it.
305
+ if (invalid) {
306
+ ariaIds.push(this._errorId);
307
+ }
308
+
309
+ this.ariaTarget.setAttribute(attr, ariaIds.join(' '));
310
+ }
311
+ }
312
+
313
+ /** @private */
314
+ __ariaChanged(invalid, helperId) {
315
+ this._updateAriaAttribute(invalid, helperId);
316
+ }
317
+ };
@@ -3,6 +3,7 @@
3
3
  * Copyright (c) 2021 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
+ import { DelegateStateMixin } from './delegate-state-mixin.js';
6
7
  import { InputMixin } from './input-mixin.js';
7
8
  import { ValidateMixin } from './validate-mixin.js';
8
9
 
@@ -17,7 +18,7 @@ interface InputConstraintsMixinConstructor {
17
18
  new (...args: any[]): InputConstraintsMixin;
18
19
  }
19
20
 
20
- interface InputConstraintsMixin extends InputMixin, ValidateMixin {
21
+ interface InputConstraintsMixin extends DelegateStateMixin, InputMixin, ValidateMixin {
21
22
  /**
22
23
  * Returns true if the current input value satisfies all constraints (if any).
23
24
  */
@@ -4,71 +4,123 @@
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';
7
+ import { DelegateStateMixin } from './delegate-state-mixin.js';
7
8
  import { InputMixin } from './input-mixin.js';
8
9
  import { ValidateMixin } from './validate-mixin.js';
9
10
 
10
- const InputConstraintsMixinImplementation = (superclass) =>
11
- class InputConstraintsMixinClass extends ValidateMixin(InputMixin(superclass)) {
12
- static get constraints() {
13
- return ['required'];
14
- }
11
+ /**
12
+ * A mixin to combine multiple input validation constraints.
13
+ *
14
+ * @polymerMixin
15
+ * @mixes DelegateStateMixin
16
+ * @mixes InputMixin
17
+ * @mixes ValidateMixin
18
+ */
19
+ export const InputConstraintsMixin = dedupingMixin(
20
+ (superclass) =>
21
+ class InputConstraintsMixinClass extends DelegateStateMixin(ValidateMixin(InputMixin(superclass))) {
22
+ /**
23
+ * An array of attributes which participate in the input validation.
24
+ * Changing these attributes will cause the input to re-validate.
25
+ *
26
+ * IMPORTANT: The attributes should be properly delegated to the input element
27
+ * from the host using `delegateAttrs` getter (see `DelegateStateMixin`).
28
+ * The `required` attribute is already delegated.
29
+ */
30
+ static get constraints() {
31
+ return ['required'];
32
+ }
33
+
34
+ static get delegateAttrs() {
35
+ return [...super.delegateAttrs, 'required'];
36
+ }
15
37
 
16
- /** @protected */
17
- ready() {
18
- super.ready();
38
+ /** @protected */
39
+ ready() {
40
+ super.ready();
19
41
 
20
- this._createConstraintsObserver();
21
- }
42
+ this._createConstraintsObserver();
43
+ }
22
44
 
23
- /**
24
- * Returns true if the current input value satisfies all constraints (if any).
25
- * @return {boolean}
26
- */
27
- checkValidity() {
28
- if (this.inputElement && this.constructor.constraints.some((c) => this.__isValidConstraint(this[c]))) {
29
- return this.inputElement.checkValidity();
30
- } else {
31
- return !this.invalid;
45
+ /**
46
+ * Returns true if the current input value satisfies all constraints (if any).
47
+ * @return {boolean}
48
+ */
49
+ checkValidity() {
50
+ if (this.inputElement && this._hasValidConstraints(this.constructor.constraints.map((c) => this[c]))) {
51
+ return this.inputElement.checkValidity();
52
+ } else {
53
+ return !this.invalid;
54
+ }
32
55
  }
33
- }
34
56
 
35
- /**
36
- * Override this method to customize setting up constraints observer.
37
- * @protected
38
- */
39
- _createConstraintsObserver() {
40
- // This complex observer needs to be added dynamically instead of using `static get observers()`
41
- // to make it possible to tweak this behavior in classes that apply this mixin.
42
- this._createMethodObserver(`_constraintsChanged(${this.constructor.constraints.join(', ')})`);
43
- }
57
+ /**
58
+ * Returns true if some of the provided set of constraints are valid.
59
+ * @param {Array} constraints
60
+ * @return {boolean}
61
+ * @protected
62
+ */
63
+ _hasValidConstraints(constraints) {
64
+ return constraints.some((c) => this.__isValidConstraint(c));
65
+ }
44
66
 
45
- /**
46
- * Override this method to implement custom validation constraints.
47
- * @param {unknown[]} constraints
48
- * @protected
49
- */
50
- _constraintsChanged(...constraints) {
51
- // Prevent marking field as invalid when setting required state
52
- // or any other constraint before a user has entered the value.
53
- if (!this.invalid) {
54
- return;
67
+ /**
68
+ * Override this method to customize setting up constraints observer.
69
+ * @protected
70
+ */
71
+ _createConstraintsObserver() {
72
+ // This complex observer needs to be added dynamically instead of using `static get observers()`
73
+ // to make it possible to tweak this behavior in classes that apply this mixin.
74
+ this._createMethodObserver(`_constraintsChanged(${this.constructor.constraints.join(', ')})`);
55
75
  }
56
76
 
57
- if (constraints.some((c) => this.__isValidConstraint(c))) {
77
+ /**
78
+ * Override this method to implement custom validation constraints.
79
+ * @param {unknown[]} constraints
80
+ * @protected
81
+ */
82
+ _constraintsChanged(...constraints) {
83
+ // Prevent marking field as invalid when setting required state
84
+ // or any other constraint before a user has entered the value.
85
+ if (!this.invalid) {
86
+ return;
87
+ }
88
+
89
+ if (this._hasValidConstraints(constraints)) {
90
+ this.validate();
91
+ } else {
92
+ this.invalid = false;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Override an event listener inherited from `InputMixin`
98
+ * to capture native `change` event and make sure that
99
+ * a new one is dispatched after validation runs.
100
+ * @param {Event} event
101
+ * @protected
102
+ * @override
103
+ */
104
+ _onChange(event) {
105
+ event.stopPropagation();
106
+
58
107
  this.validate();
59
- } else {
60
- this.invalid = false;
108
+
109
+ this.dispatchEvent(
110
+ new CustomEvent('change', {
111
+ detail: {
112
+ sourceEvent: event
113
+ },
114
+ bubbles: event.bubbles,
115
+ cancelable: event.cancelable
116
+ })
117
+ );
61
118
  }
62
- }
63
119
 
64
- /** @private */
65
- __isValidConstraint(constraint) {
66
- // 0 is valid for `minlength` and `maxlength`
67
- return Boolean(constraint) || constraint === 0;
120
+ /** @private */
121
+ __isValidConstraint(constraint) {
122
+ // 0 is valid for `minlength` and `maxlength`
123
+ return Boolean(constraint) || constraint === 0;
124
+ }
68
125
  }
69
- };
70
-
71
- /**
72
- * A mixin to combine multiple input validation constraints.
73
- */
74
- export const InputConstraintsMixin = dedupingMixin(InputConstraintsMixinImplementation);
126
+ );