@vaadin/form-layout 24.6.5 → 24.7.0-alpha10

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/form-layout",
3
- "version": "24.6.5",
3
+ "version": "24.7.0-alpha10",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -35,24 +35,26 @@
35
35
  "polymer"
36
36
  ],
37
37
  "dependencies": {
38
+ "@open-wc/dedupe-mixin": "^1.3.0",
38
39
  "@polymer/polymer": "^3.0.0",
39
- "@vaadin/a11y-base": "~24.6.5",
40
- "@vaadin/component-base": "~24.6.5",
41
- "@vaadin/vaadin-lumo-styles": "~24.6.5",
42
- "@vaadin/vaadin-material-styles": "~24.6.5",
43
- "@vaadin/vaadin-themable-mixin": "~24.6.5"
40
+ "@vaadin/a11y-base": "24.7.0-alpha10",
41
+ "@vaadin/component-base": "24.7.0-alpha10",
42
+ "@vaadin/vaadin-lumo-styles": "24.7.0-alpha10",
43
+ "@vaadin/vaadin-material-styles": "24.7.0-alpha10",
44
+ "@vaadin/vaadin-themable-mixin": "24.7.0-alpha10",
45
+ "lit": "^3.0.0"
44
46
  },
45
47
  "devDependencies": {
46
- "@vaadin/chai-plugins": "~24.6.5",
47
- "@vaadin/custom-field": "~24.6.5",
48
- "@vaadin/test-runner-commands": "~24.6.5",
48
+ "@vaadin/chai-plugins": "24.7.0-alpha10",
49
+ "@vaadin/custom-field": "24.7.0-alpha10",
50
+ "@vaadin/test-runner-commands": "24.7.0-alpha10",
49
51
  "@vaadin/testing-helpers": "^1.1.0",
50
- "@vaadin/text-field": "~24.6.5",
52
+ "@vaadin/text-field": "24.7.0-alpha10",
51
53
  "sinon": "^18.0.0"
52
54
  },
53
55
  "web-types": [
54
56
  "web-types.json",
55
57
  "web-types.lit.json"
56
58
  ],
57
- "gitHead": "fc109a4234a1f60e89717ab1c0dc8fb4451aa418"
59
+ "gitHead": "c0f8933df2a6a40648d3fb9cfbae6bbf86a8aa90"
58
60
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2025 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import type { Constructor } from '@open-wc/dedupe-mixin';
7
+
8
+ /**
9
+ * A mixin providing common form-item functionality.
10
+ */
11
+ export declare function FormItemMixin<T extends Constructor<HTMLElement>>(base: T): Constructor<FormItemMixinClass> & T;
12
+
13
+ export declare class FormItemMixinClass {
14
+ /**
15
+ * Returns a target element to add ARIA attributes to for a field.
16
+ *
17
+ * - For Vaadin field components, the method returns an element
18
+ * obtained through the `ariaTarget` property defined in `FieldMixin`.
19
+ * - In other cases, the method returns the field element itself.
20
+ */
21
+ protected _getFieldAriaTarget(field: HTMLElement): HTMLElement;
22
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2025 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { addValueToAttribute, removeValueFromAttribute } from '@vaadin/component-base/src/dom-utils.js';
7
+ import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
8
+
9
+ let spacingDeprecationNotified = false;
10
+ let labelWidthDeprecationNotified = false;
11
+ let labelSpacingDeprecationNotified = false;
12
+
13
+ /**
14
+ * @polymerMixin
15
+ */
16
+ export const FormItemMixin = (superClass) =>
17
+ class extends superClass {
18
+ constructor() {
19
+ super();
20
+ this.__updateInvalidState = this.__updateInvalidState.bind(this);
21
+
22
+ /**
23
+ * An observer for a field node to reflect its `required` and `invalid` attributes to the component.
24
+ *
25
+ * @type {MutationObserver}
26
+ * @private
27
+ */
28
+ this.__fieldNodeObserver = new MutationObserver(() => this.__updateRequiredState(this.__fieldNode.required));
29
+
30
+ /**
31
+ * The first label node in the label slot.
32
+ *
33
+ * @type {HTMLElement | null}
34
+ * @private
35
+ */
36
+ this.__labelNode = null;
37
+
38
+ /**
39
+ * The first field node in the content slot.
40
+ *
41
+ * An element is considered a field when it has the `checkValidity` or `validate` method.
42
+ *
43
+ * @type {HTMLElement | null}
44
+ * @private
45
+ */
46
+ this.__fieldNode = null;
47
+ }
48
+
49
+ /** @protected */
50
+ ready() {
51
+ super.ready();
52
+
53
+ const computedStyle = getComputedStyle(this);
54
+ const spacing = computedStyle.getPropertyValue('--vaadin-form-item-row-spacing');
55
+ const labelWidth = computedStyle.getPropertyValue('--vaadin-form-item-label-width');
56
+ const labelSpacing = computedStyle.getPropertyValue('--vaadin-form-item-label-spacing');
57
+
58
+ if (!spacingDeprecationNotified && spacing !== '' && parseInt(spacing) !== 0) {
59
+ console.warn(
60
+ '`--vaadin-form-item-row-spacing` is deprecated since 24.7. Use `--vaadin-form-layout-row-spacing` on <vaadin-form-layout> instead.',
61
+ );
62
+ spacingDeprecationNotified = true;
63
+ }
64
+
65
+ if (!labelWidthDeprecationNotified && labelWidth !== '' && parseInt(labelWidth) !== 0) {
66
+ console.warn(
67
+ '`--vaadin-form-item-label-width` is deprecated since 24.7. Use `--vaadin-form-layout-label-width` on <vaadin-form-layout> instead.',
68
+ );
69
+ labelWidthDeprecationNotified = true;
70
+ }
71
+
72
+ if (!labelSpacingDeprecationNotified && labelSpacing !== '' && parseInt(labelSpacing) !== 0) {
73
+ console.warn(
74
+ '`--vaadin-form-item-label-spacing` is deprecated since 24.7. Use `--vaadin-form-layout-label-spacing` on <vaadin-form-layout> instead.',
75
+ );
76
+ labelSpacingDeprecationNotified = true;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Returns a target element to add ARIA attributes to for a field.
82
+ *
83
+ * - For Vaadin field components, the method returns an element
84
+ * obtained through the `ariaTarget` property defined in `FieldMixin`.
85
+ * - In other cases, the method returns the field element itself.
86
+ *
87
+ * @param {HTMLElement} field
88
+ * @protected
89
+ */
90
+ _getFieldAriaTarget(field) {
91
+ return field.ariaTarget || field;
92
+ }
93
+
94
+ /**
95
+ * Links the label to a field by adding the label id to
96
+ * the `aria-labelledby` attribute of the field's ARIA target element.
97
+ *
98
+ * @param {HTMLElement} field
99
+ * @private
100
+ */
101
+ __linkLabelToField(field) {
102
+ addValueToAttribute(this._getFieldAriaTarget(field), 'aria-labelledby', this.__labelId);
103
+ }
104
+
105
+ /**
106
+ * Unlinks the label from a field by removing the label id from
107
+ * the `aria-labelledby` attribute of the field's ARIA target element.
108
+ *
109
+ * @param {HTMLElement} field
110
+ * @private
111
+ */
112
+ __unlinkLabelFromField(field) {
113
+ removeValueFromAttribute(this._getFieldAriaTarget(field), 'aria-labelledby', this.__labelId);
114
+ }
115
+
116
+ /** @private */
117
+ __onLabelClick() {
118
+ const fieldNode = this.__fieldNode;
119
+ if (fieldNode) {
120
+ fieldNode.focus();
121
+ fieldNode.click();
122
+ }
123
+ }
124
+
125
+ /** @private */
126
+ __getValidateFunction(field) {
127
+ return field.validate || field.checkValidity;
128
+ }
129
+
130
+ /**
131
+ * A `slotchange` event handler for the label slot.
132
+ *
133
+ * - Ensures the label id is only assigned to the first label node.
134
+ * - Ensures the label node is linked to the first field node via the `aria-labelledby` attribute
135
+ * if both nodes are provided, and unlinked otherwise.
136
+ *
137
+ * @private
138
+ */
139
+ __onLabelSlotChange() {
140
+ if (this.__labelNode) {
141
+ this.__labelNode = null;
142
+
143
+ if (this.__fieldNode) {
144
+ this.__unlinkLabelFromField(this.__fieldNode);
145
+ }
146
+ }
147
+
148
+ const newLabelNode = this.$.labelSlot.assignedElements()[0];
149
+ if (newLabelNode) {
150
+ this.__labelNode = newLabelNode;
151
+
152
+ if (this.__labelNode.id) {
153
+ // The new label node already has an id. Let's use it.
154
+ this.__labelId = this.__labelNode.id;
155
+ } else {
156
+ // The new label node doesn't have an id yet. Generate a unique one.
157
+ this.__labelId = `label-${this.localName}-${generateUniqueId()}`;
158
+ this.__labelNode.id = this.__labelId;
159
+ }
160
+
161
+ if (this.__fieldNode) {
162
+ this.__linkLabelToField(this.__fieldNode);
163
+ }
164
+ }
165
+ }
166
+
167
+ /**
168
+ * A `slotchange` event handler for the content slot.
169
+ *
170
+ * - Ensures the label node is only linked to the first field node via the `aria-labelledby` attribute.
171
+ * - Sets up an observer for the `required` attribute changes on the first field
172
+ * to reflect the attribute on the component. Ensures the observer is disconnected from the field
173
+ * as soon as it is removed or replaced by another one.
174
+ *
175
+ * @private
176
+ */
177
+ __onContentSlotChange() {
178
+ if (this.__fieldNode) {
179
+ // Discard the old field
180
+ this.__unlinkLabelFromField(this.__fieldNode);
181
+ this.__updateRequiredState(false);
182
+ this.__fieldNodeObserver.disconnect();
183
+ this.__fieldNode = null;
184
+ }
185
+
186
+ const fieldNodes = this.$.contentSlot.assignedElements();
187
+ if (fieldNodes.length > 1) {
188
+ console.warn(
189
+ `WARNING: Since Vaadin 23, placing multiple fields directly to a <vaadin-form-item> is deprecated.
190
+ Please wrap fields with a <vaadin-custom-field> instead.`,
191
+ );
192
+ }
193
+
194
+ const newFieldNode = fieldNodes.find((field) => {
195
+ return !!this.__getValidateFunction(field);
196
+ });
197
+ if (newFieldNode) {
198
+ this.__fieldNode = newFieldNode;
199
+ this.__updateRequiredState(this.__fieldNode.required);
200
+ this.__fieldNodeObserver.observe(this.__fieldNode, { attributes: true, attributeFilter: ['required'] });
201
+
202
+ if (this.__labelNode) {
203
+ this.__linkLabelToField(this.__fieldNode);
204
+ }
205
+ }
206
+ }
207
+
208
+ /** @private */
209
+ __updateRequiredState(required) {
210
+ if (required) {
211
+ this.setAttribute('required', '');
212
+ this.__fieldNode.addEventListener('blur', this.__updateInvalidState);
213
+ this.__fieldNode.addEventListener('change', this.__updateInvalidState);
214
+ } else {
215
+ this.removeAttribute('invalid');
216
+ this.removeAttribute('required');
217
+ this.__fieldNode.removeEventListener('blur', this.__updateInvalidState);
218
+ this.__fieldNode.removeEventListener('change', this.__updateInvalidState);
219
+ }
220
+ }
221
+
222
+ /** @private */
223
+ __updateInvalidState() {
224
+ const isValid = this.__getValidateFunction(this.__fieldNode).call(this.__fieldNode);
225
+ this.toggleAttribute('invalid', isValid === false);
226
+ }
227
+ };
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2017 - 2024 Vaadin Ltd.
3
+ * Copyright (c) 2017 - 2025 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 { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
7
+ import { FormItemMixin } from './vaadin-form-item-mixin.js';
7
8
 
8
9
  /**
9
10
  * `<vaadin-form-item>` is a Web Component providing labelled form item wrapper
@@ -42,6 +43,10 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
42
43
  * because the `label-position` attribute is triggered automatically by the parent
43
44
  * `<vaadin-form-layout>`, depending on its width and responsive behavior.
44
45
  *
46
+ * **Deprecation note:** The `label-position` attribute is deprecated since 24.7 and
47
+ * will be removed in Vaadin 25, when a new approach for setting the label position
48
+ * will be introduced.
49
+ *
45
50
  * ### Input Width
46
51
  *
47
52
  * By default, `<vaadin-form-item>` does not manipulate the width of the slotted
@@ -65,6 +70,10 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
65
70
  * }
66
71
  * ```
67
72
  *
73
+ * **Deprecation note:** The `label-position` attribute is deprecated since 24.7 and
74
+ * will be removed in Vaadin 25, when a new approach to styling the form-item
75
+ * based on the label position will be introduced.
76
+ *
68
77
  * The following shadow DOM parts are available for styling:
69
78
  *
70
79
  * Part name | Description
@@ -78,22 +87,13 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
78
87
  *
79
88
  * Custom CSS property | Description | Default
80
89
  * ---|---|---
81
- * `--vaadin-form-item-label-width` | Width of the label column when the labels are aside | `8em`
82
- * `--vaadin-form-item-label-spacing` | Spacing between the label column and the input column when the labels are aside | `1em`
83
- * `--vaadin-form-item-row-spacing` | Height of the spacing between the form item elements | `1em`
90
+ * `--vaadin-form-item-label-width` | (DEPRECATED: Use `--vaadin-form-layout-label-width` on `<vaadin-form-layout>` instead) Width of the label column when the labels are aside | `8em`
91
+ * `--vaadin-form-item-label-spacing` | (DEPRECATED: Use `--vaadin-form-layout-label-spacing` on `<vaadin-form-layout>` instead) Spacing between the label column and the input column when the labels are aside | `1em`
92
+ * `--vaadin-form-item-row-spacing` | (DEPRECATED: Use `--vaadin-form-layout-row-spacing` on `<vaadin-form-layout>` instead) Height of the spacing between the form item elements | `1em`
84
93
  *
85
94
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
86
95
  */
87
- declare class FormItem extends ThemableMixin(HTMLElement) {
88
- /**
89
- * Returns a target element to add ARIA attributes to for a field.
90
- *
91
- * - For Vaadin field components, the method returns an element
92
- * obtained through the `ariaTarget` property defined in `FieldMixin`.
93
- * - In other cases, the method returns the field element itself.
94
- */
95
- protected _getFieldAriaTarget(field: HTMLElement): HTMLElement;
96
- }
96
+ declare class FormItem extends FormItemMixin(ThemableMixin(HTMLElement)) {}
97
97
 
98
98
  declare global {
99
99
  interface HTMLElementTagNameMap {
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2017 - 2024 Vaadin Ltd.
3
+ * Copyright (c) 2017 - 2025 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 { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
7
7
  import { defineCustomElement } from '@vaadin/component-base/src/define.js';
8
- import { addValueToAttribute, removeValueFromAttribute } from '@vaadin/component-base/src/dom-utils.js';
9
- import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
10
- import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
8
+ import { registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
9
+ import { FormItemMixin } from './vaadin-form-item-mixin.js';
10
+ import { formItemStyles } from './vaadin-form-layout-styles.js';
11
+
12
+ registerStyles('vaadin-form-item', formItemStyles, { moduleId: 'vaadin-form-item-styles' });
11
13
 
12
14
  /**
13
15
  * `<vaadin-form-item>` is a Web Component providing labelled form item wrapper
@@ -46,6 +48,10 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
46
48
  * because the `label-position` attribute is triggered automatically by the parent
47
49
  * `<vaadin-form-layout>`, depending on its width and responsive behavior.
48
50
  *
51
+ * **Deprecation note:** The `label-position` attribute is deprecated since 24.7 and
52
+ * will be removed in Vaadin 25, when a new approach for setting the label position
53
+ * will be introduced.
54
+ *
49
55
  * ### Input Width
50
56
  *
51
57
  * By default, `<vaadin-form-item>` does not manipulate the width of the slotted
@@ -69,6 +75,10 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
69
75
  * }
70
76
  * ```
71
77
  *
78
+ * **Deprecation note:** The `label-position` attribute is deprecated since 24.7 and
79
+ * will be removed in Vaadin 25, when a new approach to styling the form-item
80
+ * based on the label position will be introduced.
81
+ *
72
82
  * The following shadow DOM parts are available for styling:
73
83
  *
74
84
  * Part name | Description
@@ -82,60 +92,24 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
82
92
  *
83
93
  * Custom CSS property | Description | Default
84
94
  * ---|---|---
85
- * `--vaadin-form-item-label-width` | Width of the label column when the labels are aside | `8em`
86
- * `--vaadin-form-item-label-spacing` | Spacing between the label column and the input column when the labels are aside | `1em`
87
- * `--vaadin-form-item-row-spacing` | Height of the spacing between the form item elements | `1em`
95
+ * `--vaadin-form-item-label-width` | (DEPRECATED: Use `--vaadin-form-layout-label-width` on `<vaadin-form-layout>` instead) Width of the label column when the labels are aside | `8em`
96
+ * `--vaadin-form-item-label-spacing` | (DEPRECATED: Use `--vaadin-form-layout-label-spacing` on `<vaadin-form-layout>` instead) Spacing between the label column and the input column when the labels are aside | `1em`
97
+ * `--vaadin-form-item-row-spacing` | (DEPRECATED: Use `--vaadin-form-layout-row-spacing` on `<vaadin-form-layout>` instead) Height of the spacing between the form item elements | `1em`
88
98
  *
89
99
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
90
100
  *
91
101
  * @customElement
92
102
  * @extends HTMLElement
103
+ * @mixes FormItemMixin
93
104
  * @mixes ThemableMixin
94
105
  */
95
- class FormItem extends ThemableMixin(PolymerElement) {
106
+ class FormItem extends FormItemMixin(ThemableMixin(PolymerElement)) {
107
+ static get is() {
108
+ return 'vaadin-form-item';
109
+ }
110
+
96
111
  static get template() {
97
112
  return html`
98
- <style>
99
- :host {
100
- display: inline-flex;
101
- flex-direction: row;
102
- align-items: baseline;
103
- margin: calc(0.5 * var(--vaadin-form-item-row-spacing, 1em)) 0;
104
- }
105
-
106
- :host([label-position='top']) {
107
- flex-direction: column;
108
- align-items: stretch;
109
- }
110
-
111
- :host([hidden]) {
112
- display: none !important;
113
- }
114
-
115
- #label {
116
- width: var(--vaadin-form-item-label-width, 8em);
117
- flex: 0 0 auto;
118
- }
119
-
120
- :host([label-position='top']) #label {
121
- width: auto;
122
- }
123
-
124
- #spacing {
125
- width: var(--vaadin-form-item-label-spacing, 1em);
126
- flex: 0 0 auto;
127
- }
128
-
129
- #content {
130
- flex: 1 1 auto;
131
- }
132
-
133
- #content ::slotted(.full-width) {
134
- box-sizing: border-box;
135
- width: 100%;
136
- min-width: 0;
137
- }
138
- </style>
139
113
  <div id="label" part="label" on-click="__onLabelClick">
140
114
  <slot name="label" id="labelSlot" on-slotchange="__onLabelSlotChange"></slot>
141
115
  <span part="required-indicator" aria-hidden="true"></span>
@@ -146,189 +120,6 @@ class FormItem extends ThemableMixin(PolymerElement) {
146
120
  </div>
147
121
  `;
148
122
  }
149
-
150
- static get is() {
151
- return 'vaadin-form-item';
152
- }
153
-
154
- constructor() {
155
- super();
156
- this.__updateInvalidState = this.__updateInvalidState.bind(this);
157
-
158
- /**
159
- * An observer for a field node to reflect its `required` and `invalid` attributes to the component.
160
- *
161
- * @type {MutationObserver}
162
- * @private
163
- */
164
- this.__fieldNodeObserver = new MutationObserver(() => this.__updateRequiredState(this.__fieldNode.required));
165
-
166
- /**
167
- * The first label node in the label slot.
168
- *
169
- * @type {HTMLElement | null}
170
- * @private
171
- */
172
- this.__labelNode = null;
173
-
174
- /**
175
- * The first field node in the content slot.
176
- *
177
- * An element is considered a field when it has the `checkValidity` or `validate` method.
178
- *
179
- * @type {HTMLElement | null}
180
- * @private
181
- */
182
- this.__fieldNode = null;
183
- }
184
-
185
- /**
186
- * Returns a target element to add ARIA attributes to for a field.
187
- *
188
- * - For Vaadin field components, the method returns an element
189
- * obtained through the `ariaTarget` property defined in `FieldMixin`.
190
- * - In other cases, the method returns the field element itself.
191
- *
192
- * @param {HTMLElement} field
193
- * @protected
194
- */
195
- _getFieldAriaTarget(field) {
196
- return field.ariaTarget || field;
197
- }
198
-
199
- /**
200
- * Links the label to a field by adding the label id to
201
- * the `aria-labelledby` attribute of the field's ARIA target element.
202
- *
203
- * @param {HTMLElement} field
204
- * @private
205
- */
206
- __linkLabelToField(field) {
207
- addValueToAttribute(this._getFieldAriaTarget(field), 'aria-labelledby', this.__labelId);
208
- }
209
-
210
- /**
211
- * Unlinks the label from a field by removing the label id from
212
- * the `aria-labelledby` attribute of the field's ARIA target element.
213
- *
214
- * @param {HTMLElement} field
215
- * @private
216
- */
217
- __unlinkLabelFromField(field) {
218
- removeValueFromAttribute(this._getFieldAriaTarget(field), 'aria-labelledby', this.__labelId);
219
- }
220
-
221
- /** @private */
222
- __onLabelClick() {
223
- const fieldNode = this.__fieldNode;
224
- if (fieldNode) {
225
- fieldNode.focus();
226
- fieldNode.click();
227
- }
228
- }
229
-
230
- /** @private */
231
- __getValidateFunction(field) {
232
- return field.validate || field.checkValidity;
233
- }
234
-
235
- /**
236
- * A `slotchange` event handler for the label slot.
237
- *
238
- * - Ensures the label id is only assigned to the first label node.
239
- * - Ensures the label node is linked to the first field node via the `aria-labelledby` attribute
240
- * if both nodes are provided, and unlinked otherwise.
241
- *
242
- * @private
243
- */
244
- __onLabelSlotChange() {
245
- if (this.__labelNode) {
246
- this.__labelNode = null;
247
-
248
- if (this.__fieldNode) {
249
- this.__unlinkLabelFromField(this.__fieldNode);
250
- }
251
- }
252
-
253
- const newLabelNode = this.$.labelSlot.assignedElements()[0];
254
- if (newLabelNode) {
255
- this.__labelNode = newLabelNode;
256
-
257
- if (this.__labelNode.id) {
258
- // The new label node already has an id. Let's use it.
259
- this.__labelId = this.__labelNode.id;
260
- } else {
261
- // The new label node doesn't have an id yet. Generate a unique one.
262
- this.__labelId = `label-${this.localName}-${generateUniqueId()}`;
263
- this.__labelNode.id = this.__labelId;
264
- }
265
-
266
- if (this.__fieldNode) {
267
- this.__linkLabelToField(this.__fieldNode);
268
- }
269
- }
270
- }
271
-
272
- /**
273
- * A `slotchange` event handler for the content slot.
274
- *
275
- * - Ensures the label node is only linked to the first field node via the `aria-labelledby` attribute.
276
- * - Sets up an observer for the `required` attribute changes on the first field
277
- * to reflect the attribute on the component. Ensures the observer is disconnected from the field
278
- * as soon as it is removed or replaced by another one.
279
- *
280
- * @private
281
- */
282
- __onContentSlotChange() {
283
- if (this.__fieldNode) {
284
- // Discard the old field
285
- this.__unlinkLabelFromField(this.__fieldNode);
286
- this.__updateRequiredState(false);
287
- this.__fieldNodeObserver.disconnect();
288
- this.__fieldNode = null;
289
- }
290
-
291
- const fieldNodes = this.$.contentSlot.assignedElements();
292
- if (fieldNodes.length > 1) {
293
- console.warn(
294
- `WARNING: Since Vaadin 23, placing multiple fields directly to a <vaadin-form-item> is deprecated.
295
- Please wrap fields with a <vaadin-custom-field> instead.`,
296
- );
297
- }
298
-
299
- const newFieldNode = fieldNodes.find((field) => {
300
- return !!this.__getValidateFunction(field);
301
- });
302
- if (newFieldNode) {
303
- this.__fieldNode = newFieldNode;
304
- this.__updateRequiredState(this.__fieldNode.required);
305
- this.__fieldNodeObserver.observe(this.__fieldNode, { attributes: true, attributeFilter: ['required'] });
306
-
307
- if (this.__labelNode) {
308
- this.__linkLabelToField(this.__fieldNode);
309
- }
310
- }
311
- }
312
-
313
- /** @private */
314
- __updateRequiredState(required) {
315
- if (required) {
316
- this.setAttribute('required', '');
317
- this.__fieldNode.addEventListener('blur', this.__updateInvalidState);
318
- this.__fieldNode.addEventListener('change', this.__updateInvalidState);
319
- } else {
320
- this.removeAttribute('invalid');
321
- this.removeAttribute('required');
322
- this.__fieldNode.removeEventListener('blur', this.__updateInvalidState);
323
- this.__fieldNode.removeEventListener('change', this.__updateInvalidState);
324
- }
325
- }
326
-
327
- /** @private */
328
- __updateInvalidState() {
329
- const isValid = this.__getValidateFunction(this.__fieldNode).call(this.__fieldNode);
330
- this.toggleAttribute('invalid', isValid === false);
331
- }
332
123
  }
333
124
 
334
125
  defineCustomElement(FormItem);