@vaadin/form-layout 24.7.2 → 24.8.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.7.2",
3
+ "version": "24.8.0-alpha10",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -37,24 +37,24 @@
37
37
  "dependencies": {
38
38
  "@open-wc/dedupe-mixin": "^1.3.0",
39
39
  "@polymer/polymer": "^3.0.0",
40
- "@vaadin/a11y-base": "~24.7.2",
41
- "@vaadin/component-base": "~24.7.2",
42
- "@vaadin/vaadin-lumo-styles": "~24.7.2",
43
- "@vaadin/vaadin-material-styles": "~24.7.2",
44
- "@vaadin/vaadin-themable-mixin": "~24.7.2",
40
+ "@vaadin/a11y-base": "24.8.0-alpha10",
41
+ "@vaadin/component-base": "24.8.0-alpha10",
42
+ "@vaadin/vaadin-lumo-styles": "24.8.0-alpha10",
43
+ "@vaadin/vaadin-material-styles": "24.8.0-alpha10",
44
+ "@vaadin/vaadin-themable-mixin": "24.8.0-alpha10",
45
45
  "lit": "^3.0.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@vaadin/chai-plugins": "~24.7.2",
49
- "@vaadin/custom-field": "~24.7.2",
50
- "@vaadin/test-runner-commands": "~24.7.2",
48
+ "@vaadin/chai-plugins": "24.8.0-alpha10",
49
+ "@vaadin/custom-field": "24.8.0-alpha10",
50
+ "@vaadin/test-runner-commands": "24.8.0-alpha10",
51
51
  "@vaadin/testing-helpers": "^1.1.0",
52
- "@vaadin/text-field": "~24.7.2",
52
+ "@vaadin/text-field": "24.8.0-alpha10",
53
53
  "sinon": "^18.0.0"
54
54
  },
55
55
  "web-types": [
56
56
  "web-types.json",
57
57
  "web-types.lit.json"
58
58
  ],
59
- "gitHead": "b9ad056bb7367f9cfcb44eac20e12c8c998ff979"
59
+ "gitHead": "f8c79ffc67eccc3ade226dfe52fbf7d3d46428cf"
60
60
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2025 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+
7
+ /**
8
+ * An abstract class for layout implementation. Not intended for public use.
9
+ *
10
+ * @private
11
+ */
12
+ export class AbstractLayout {
13
+ /**
14
+ * @param {HTMLElement} host
15
+ * @param {{ mutationObserverOptions: MutationObserverInit }} config
16
+ */
17
+ constructor(host, config) {
18
+ this.host = host;
19
+ this.props = {};
20
+ this.config = config;
21
+ this.isConnected = false;
22
+
23
+ /** @private */
24
+ this.__resizeObserver = new ResizeObserver((entries) => setTimeout(() => this._onResize(entries)));
25
+
26
+ /** @private */
27
+ this.__mutationObserver = new MutationObserver((records) => this._onMutation(records));
28
+ }
29
+
30
+ /**
31
+ * Connects the layout to the host element.
32
+ */
33
+ connect() {
34
+ if (this.isConnected) {
35
+ return;
36
+ }
37
+
38
+ this.isConnected = true;
39
+ this.__resizeObserver.observe(this.host);
40
+ this.__mutationObserver.observe(this.host, this.config.mutationObserverOptions);
41
+ }
42
+
43
+ /**
44
+ * Disconnects the layout from the host element.
45
+ */
46
+ disconnect() {
47
+ if (!this.isConnected) {
48
+ return;
49
+ }
50
+
51
+ this.isConnected = false;
52
+ this.__resizeObserver.disconnect();
53
+ this.__mutationObserver.disconnect();
54
+ }
55
+
56
+ /**
57
+ * Sets the properties of the layout controller.
58
+ */
59
+ setProps(props) {
60
+ this.props = props;
61
+ }
62
+
63
+ /**
64
+ * Updates the layout based on the current properties.
65
+ */
66
+ updateLayout() {
67
+ // To be implemented
68
+ }
69
+
70
+ /**
71
+ * @param {ResizeObserverEntry[]} _entries
72
+ * @protected
73
+ */
74
+ _onResize(_entries) {
75
+ // To be implemented
76
+ }
77
+
78
+ /**
79
+ * @param {MutationRecord[]} _records
80
+ * @protected
81
+ */
82
+ _onMutation(_records) {
83
+ // To be implemented
84
+ }
85
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2025 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { isElementHidden } from '@vaadin/a11y-base/src/focus-utils.js';
7
+ import { AbstractLayout } from './abstract-layout.js';
8
+
9
+ /**
10
+ * Check if the node is a line break element.
11
+ *
12
+ * @param {HTMLElement} el
13
+ * @return {boolean}
14
+ */
15
+ function isBreakLine(el) {
16
+ return el.localName === 'br';
17
+ }
18
+
19
+ /**
20
+ * A class that implements the auto-responsive layout algorithm.
21
+ * Not intended for public use.
22
+ *
23
+ * @private
24
+ */
25
+ export class AutoResponsiveLayout extends AbstractLayout {
26
+ constructor(host) {
27
+ super(host, {
28
+ mutationObserverOptions: {
29
+ subtree: true,
30
+ childList: true,
31
+ attributes: true,
32
+ attributeFilter: ['colspan', 'data-colspan', 'hidden'],
33
+ },
34
+ });
35
+ }
36
+
37
+ /** @override */
38
+ connect() {
39
+ if (this.isConnected) {
40
+ return;
41
+ }
42
+
43
+ super.connect();
44
+
45
+ this.updateLayout();
46
+ }
47
+
48
+ /** @override */
49
+ disconnect() {
50
+ if (!this.isConnected) {
51
+ return;
52
+ }
53
+
54
+ super.disconnect();
55
+
56
+ const { host } = this;
57
+ host.style.removeProperty('--_column-width');
58
+ host.style.removeProperty('--_max-columns');
59
+ host.$.layout.removeAttribute('fits-labels-aside');
60
+ host.$.layout.style.removeProperty('--_grid-rendered-column-count');
61
+
62
+ this.__children.forEach((child) => {
63
+ child.style.removeProperty('--_grid-colstart');
64
+ child.style.removeProperty('--_grid-colspan');
65
+ });
66
+ }
67
+
68
+ /** @override */
69
+ setProps(props) {
70
+ super.setProps(props);
71
+
72
+ if (this.isConnected) {
73
+ this.updateLayout();
74
+ }
75
+ }
76
+
77
+ /** @override */
78
+ updateLayout() {
79
+ const { host, props } = this;
80
+ if (!this.isConnected || isElementHidden(host)) {
81
+ return;
82
+ }
83
+
84
+ let columnCount = 0;
85
+ let maxColumns = 0;
86
+
87
+ const children = this.__children;
88
+ children
89
+ .filter((child) => isBreakLine(child) || !isElementHidden(child))
90
+ .forEach((child, index, children) => {
91
+ const prevChild = children[index - 1];
92
+
93
+ if (isBreakLine(child)) {
94
+ columnCount = 0;
95
+ return;
96
+ }
97
+
98
+ if (
99
+ (prevChild && prevChild.parentElement !== child.parentElement) ||
100
+ (!props.autoRows && child.parentElement === host)
101
+ ) {
102
+ columnCount = 0;
103
+ }
104
+
105
+ if (props.autoRows && columnCount === 0) {
106
+ child.style.setProperty('--_grid-colstart', 1);
107
+ } else {
108
+ child.style.removeProperty('--_grid-colstart');
109
+ }
110
+
111
+ const colspan = child.getAttribute('colspan') || child.getAttribute('data-colspan');
112
+ if (colspan) {
113
+ columnCount += parseInt(colspan);
114
+ child.style.setProperty('--_grid-colspan', colspan);
115
+ } else {
116
+ columnCount += 1;
117
+ child.style.removeProperty('--_grid-colspan');
118
+ }
119
+
120
+ maxColumns = Math.max(maxColumns, columnCount);
121
+ });
122
+
123
+ children.filter(isElementHidden).forEach((child) => {
124
+ child.style.removeProperty('--_grid-colstart');
125
+ });
126
+
127
+ if (props.columnWidth) {
128
+ host.style.setProperty('--_column-width', props.columnWidth);
129
+ } else {
130
+ host.style.removeProperty('--_column-width');
131
+ }
132
+
133
+ host.style.setProperty('--_max-columns', Math.min(props.maxColumns, maxColumns));
134
+
135
+ host.$.layout.toggleAttribute('fits-labels-aside', this.props.labelsAside && this.__fitsLabelsAside);
136
+ host.$.layout.style.setProperty('--_grid-rendered-column-count', this.__renderedColumnCount);
137
+ }
138
+
139
+ /** @override */
140
+ _onResize() {
141
+ this.updateLayout();
142
+ }
143
+
144
+ /** @override */
145
+ _onMutation(records) {
146
+ const shouldUpdateLayout = records.some(({ target }) => {
147
+ return (
148
+ target === this.host ||
149
+ target.parentElement === this.host ||
150
+ target.parentElement.localName === 'vaadin-form-row'
151
+ );
152
+ });
153
+ if (shouldUpdateLayout) {
154
+ this.updateLayout();
155
+ }
156
+ }
157
+
158
+ /** @private */
159
+ get __children() {
160
+ return [...this.host.children].flatMap((child) => {
161
+ return child.localName === 'vaadin-form-row' ? [...child.children] : child;
162
+ });
163
+ }
164
+
165
+ /** @private */
166
+ get __renderedColumnCount() {
167
+ // Calculate the number of rendered columns, excluding CSS grid auto columns (0px)
168
+ const { gridTemplateColumns } = getComputedStyle(this.host.$.layout);
169
+ return gridTemplateColumns.split(' ').filter((width) => width !== '0px').length;
170
+ }
171
+
172
+ /** @private */
173
+ get __columnWidthWithLabelsAside() {
174
+ const { backgroundPositionY } = getComputedStyle(this.host.$.layout, '::before');
175
+ return parseFloat(backgroundPositionY);
176
+ }
177
+
178
+ /** @private */
179
+ get __fitsLabelsAside() {
180
+ return this.host.offsetWidth >= this.__columnWidthWithLabelsAside;
181
+ }
182
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2025 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { isElementHidden } from '@vaadin/a11y-base/src/focus-utils.js';
7
+ import { AbstractLayout } from './abstract-layout.js';
8
+
9
+ function isValidCSSLength(value) {
10
+ // Check if the value is a valid CSS length and not `inherit` or `normal`,
11
+ // which are also valid values for `word-spacing`, see:
12
+ // https://drafts.csswg.org/css-text-3/#word-spacing-property
13
+ return CSS.supports('word-spacing', value) && !['inherit', 'normal'].includes(value);
14
+ }
15
+
16
+ function naturalNumberOrOne(n) {
17
+ if (typeof n === 'number' && n >= 1 && n < Infinity) {
18
+ return Math.floor(n);
19
+ }
20
+ return 1;
21
+ }
22
+
23
+ /**
24
+ * A class that implements the layout algorithm based on responsive steps.
25
+ * Not intended for public use.
26
+ *
27
+ * @private
28
+ */
29
+ export class ResponsiveStepsLayout extends AbstractLayout {
30
+ constructor(host) {
31
+ super(host, {
32
+ mutationObserverOptions: {
33
+ subtree: true,
34
+ childList: true,
35
+ attributes: true,
36
+ attributeFilter: ['colspan', 'data-colspan', 'hidden'],
37
+ },
38
+ });
39
+ }
40
+
41
+ /** @override */
42
+ connect() {
43
+ if (this.isConnected) {
44
+ return;
45
+ }
46
+
47
+ super.connect();
48
+
49
+ this.__selectResponsiveStep();
50
+ this.updateLayout();
51
+
52
+ requestAnimationFrame(() => this.__selectResponsiveStep());
53
+ requestAnimationFrame(() => this.updateLayout());
54
+ }
55
+
56
+ /** @override */
57
+ disconnect() {
58
+ if (!this.isConnected) {
59
+ return;
60
+ }
61
+
62
+ super.disconnect();
63
+
64
+ const { host } = this;
65
+ host.$.layout.style.removeProperty('opacity');
66
+ [...host.children].forEach((child) => {
67
+ child.style.removeProperty('width');
68
+ child.style.removeProperty('margin-left');
69
+ child.style.removeProperty('margin-right');
70
+ child.removeAttribute('label-position');
71
+ });
72
+ }
73
+
74
+ /** @override */
75
+ setProps(props) {
76
+ const { responsiveSteps } = props;
77
+ if (!Array.isArray(responsiveSteps)) {
78
+ throw new Error('Invalid "responsiveSteps" type, an Array is required.');
79
+ }
80
+
81
+ if (responsiveSteps.length < 1) {
82
+ throw new Error('Invalid empty "responsiveSteps" array, at least one item is required.');
83
+ }
84
+
85
+ responsiveSteps.forEach((step) => {
86
+ if (naturalNumberOrOne(step.columns) !== step.columns) {
87
+ throw new Error(`Invalid 'columns' value of ${step.columns}, a natural number is required.`);
88
+ }
89
+
90
+ if (step.minWidth !== undefined && !isValidCSSLength(step.minWidth)) {
91
+ throw new Error(`Invalid 'minWidth' value of ${step.minWidth}, a valid CSS length required.`);
92
+ }
93
+
94
+ if (step.labelsPosition !== undefined && ['aside', 'top'].indexOf(step.labelsPosition) === -1) {
95
+ throw new Error(
96
+ `Invalid 'labelsPosition' value of ${step.labelsPosition}, 'aside' or 'top' string is required.`,
97
+ );
98
+ }
99
+ });
100
+
101
+ super.setProps(props);
102
+
103
+ if (this.isConnected) {
104
+ this.__selectResponsiveStep();
105
+ this.updateLayout();
106
+ }
107
+ }
108
+
109
+ /** @override */
110
+ updateLayout() {
111
+ const { host } = this;
112
+
113
+ // Do not update layout when invisible
114
+ if (!this.isConnected || isElementHidden(host)) {
115
+ return;
116
+ }
117
+
118
+ /*
119
+ The item width formula:
120
+
121
+ itemWidth = colspan / columnCount * 100% - columnSpacing
122
+
123
+ We have to subtract columnSpacing, because the column spacing space is taken
124
+ by item margins of 1/2 * spacing on both sides
125
+ */
126
+
127
+ const style = getComputedStyle(host);
128
+ const columnSpacing = style.getPropertyValue('--vaadin-form-layout-column-spacing');
129
+
130
+ const direction = style.direction;
131
+ const marginStartProp = `margin-${direction === 'ltr' ? 'left' : 'right'}`;
132
+ const marginEndProp = `margin-${direction === 'ltr' ? 'right' : 'left'}`;
133
+
134
+ const containerWidth = host.offsetWidth;
135
+
136
+ let col = 0;
137
+ Array.from(host.children)
138
+ .filter((child) => child.localName === 'br' || getComputedStyle(child).display !== 'none')
139
+ .forEach((child, index, children) => {
140
+ if (child.localName === 'br') {
141
+ // Reset column count on line break
142
+ col = 0;
143
+ return;
144
+ }
145
+
146
+ const attrColspan = child.getAttribute('colspan') || child.getAttribute('data-colspan');
147
+ let colspan;
148
+ colspan = naturalNumberOrOne(parseFloat(attrColspan));
149
+
150
+ // Never span further than the number of columns
151
+ colspan = Math.min(colspan, this.__columnCount);
152
+
153
+ const childRatio = colspan / this.__columnCount;
154
+ child.style.width = `calc(${childRatio * 100}% - ${1 - childRatio} * ${columnSpacing})`;
155
+
156
+ if (col + colspan > this.__columnCount) {
157
+ // Too big to fit on this row, let's wrap it
158
+ col = 0;
159
+ }
160
+
161
+ // At the start edge
162
+ if (col === 0) {
163
+ child.style.setProperty(marginStartProp, '0px');
164
+ } else {
165
+ child.style.removeProperty(marginStartProp);
166
+ }
167
+
168
+ const nextIndex = index + 1;
169
+ const nextLineBreak = nextIndex < children.length && children[nextIndex].localName === 'br';
170
+
171
+ // At the end edge
172
+ if (col + colspan === this.__columnCount) {
173
+ child.style.setProperty(marginEndProp, '0px');
174
+ } else if (nextLineBreak) {
175
+ const colspanRatio = (this.__columnCount - col - colspan) / this.__columnCount;
176
+ child.style.setProperty(
177
+ marginEndProp,
178
+ `calc(${colspanRatio * containerWidth}px + ${colspanRatio} * ${columnSpacing})`,
179
+ );
180
+ } else {
181
+ child.style.removeProperty(marginEndProp);
182
+ }
183
+
184
+ // Move the column counter
185
+ col = (col + colspan) % this.__columnCount;
186
+
187
+ if (child.localName === 'vaadin-form-item') {
188
+ if (this.__labelsOnTop) {
189
+ if (child.getAttribute('label-position') !== 'top') {
190
+ child.__useLayoutLabelPosition = true;
191
+ child.setAttribute('label-position', 'top');
192
+ }
193
+ } else if (child.__useLayoutLabelPosition) {
194
+ delete child.__useLayoutLabelPosition;
195
+ child.removeAttribute('label-position');
196
+ }
197
+ }
198
+ });
199
+ }
200
+
201
+ /** @override */
202
+ _onResize() {
203
+ const { host } = this;
204
+ if (isElementHidden(host)) {
205
+ host.$.layout.style.opacity = '0';
206
+ return;
207
+ }
208
+
209
+ this.__selectResponsiveStep();
210
+ this.updateLayout();
211
+
212
+ host.$.layout.style.opacity = '';
213
+ }
214
+
215
+ /** @override */
216
+ _onMutation(records) {
217
+ const shouldUpdateLayout = records.some(({ target }) => {
218
+ return target === this.host || target.parentElement === this.host;
219
+ });
220
+ if (shouldUpdateLayout) {
221
+ this.updateLayout();
222
+ }
223
+ }
224
+
225
+ /** @private */
226
+ __selectResponsiveStep() {
227
+ if (!this.isConnected) {
228
+ return;
229
+ }
230
+
231
+ const { host, props } = this;
232
+ // Iterate through responsiveSteps and choose the step
233
+ let selectedStep;
234
+ const tmpStyleProp = 'background-position';
235
+ props.responsiveSteps.forEach((step) => {
236
+ // Convert minWidth to px units for comparison
237
+ host.$.layout.style.setProperty(tmpStyleProp, step.minWidth);
238
+ const stepMinWidthPx = parseFloat(getComputedStyle(host.$.layout).getPropertyValue(tmpStyleProp));
239
+
240
+ // Compare step min-width with the host width, select the passed step
241
+ if (stepMinWidthPx <= host.offsetWidth) {
242
+ selectedStep = step;
243
+ }
244
+ });
245
+ host.$.layout.style.removeProperty(tmpStyleProp);
246
+
247
+ // Sometimes converting units is not possible, e.g, when element is
248
+ // not connected. Then the `selectedStep` stays `undefined`.
249
+ if (selectedStep) {
250
+ // Apply the chosen responsive step's properties
251
+ this.__columnCount = selectedStep.columns;
252
+ this.__labelsOnTop = selectedStep.labelsPosition === 'top';
253
+ }
254
+ }
255
+ }
@@ -17,7 +17,8 @@ export const FormItemMixin = (superClass) =>
17
17
  class extends superClass {
18
18
  constructor() {
19
19
  super();
20
- this.__updateInvalidState = this.__updateInvalidState.bind(this);
20
+
21
+ this.__onFieldInteraction = this.__onFieldInteraction.bind(this);
21
22
 
22
23
  /**
23
24
  * An observer for a field node to reflect its `required` and `invalid` attributes to the component.
@@ -25,7 +26,7 @@ export const FormItemMixin = (superClass) =>
25
26
  * @type {MutationObserver}
26
27
  * @private
27
28
  */
28
- this.__fieldNodeObserver = new MutationObserver(() => this.__updateRequiredState(this.__fieldNode.required));
29
+ this.__fieldNodeObserver = new MutationObserver(() => this.__synchronizeAttributes());
29
30
 
30
31
  /**
31
32
  * The first label node in the label slot.
@@ -44,6 +45,8 @@ export const FormItemMixin = (superClass) =>
44
45
  * @private
45
46
  */
46
47
  this.__fieldNode = null;
48
+
49
+ this.__isFieldDirty = false;
47
50
  }
48
51
 
49
52
  /** @protected */
@@ -122,11 +125,6 @@ export const FormItemMixin = (superClass) =>
122
125
  }
123
126
  }
124
127
 
125
- /** @private */
126
- __getValidateFunction(field) {
127
- return field.validate || field.checkValidity;
128
- }
129
-
130
128
  /**
131
129
  * A `slotchange` event handler for the label slot.
132
130
  *
@@ -178,9 +176,11 @@ export const FormItemMixin = (superClass) =>
178
176
  if (this.__fieldNode) {
179
177
  // Discard the old field
180
178
  this.__unlinkLabelFromField(this.__fieldNode);
181
- this.__updateRequiredState(false);
182
179
  this.__fieldNodeObserver.disconnect();
180
+ this.__fieldNode.removeEventListener('blur', this.__onFieldInteraction);
181
+ this.__fieldNode.removeEventListener('change', this.__onFieldInteraction);
183
182
  this.__fieldNode = null;
183
+ this.__isFieldDirty = false;
184
184
  }
185
185
 
186
186
  const fieldNodes = this.$.contentSlot.assignedElements();
@@ -191,37 +191,43 @@ Please wrap fields with a <vaadin-custom-field> instead.`,
191
191
  );
192
192
  }
193
193
 
194
- const newFieldNode = fieldNodes.find((field) => {
195
- return !!this.__getValidateFunction(field);
196
- });
194
+ const newFieldNode = fieldNodes.find((field) => field.validate || field.checkValidity);
197
195
  if (newFieldNode) {
198
196
  this.__fieldNode = newFieldNode;
199
- this.__updateRequiredState(this.__fieldNode.required);
200
- this.__fieldNodeObserver.observe(this.__fieldNode, { attributes: true, attributeFilter: ['required'] });
197
+ this.__fieldNode.addEventListener('blur', this.__onFieldInteraction);
198
+ this.__fieldNode.addEventListener('change', this.__onFieldInteraction);
199
+ this.__fieldNodeObserver.observe(this.__fieldNode, {
200
+ attributes: true,
201
+ attributeFilter: ['required', 'invalid'],
202
+ });
201
203
 
202
204
  if (this.__labelNode) {
203
205
  this.__linkLabelToField(this.__fieldNode);
204
206
  }
205
207
  }
208
+
209
+ this.__synchronizeAttributes();
206
210
  }
207
211
 
208
212
  /** @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
- }
213
+ __onFieldInteraction() {
214
+ this.__isFieldDirty = true;
215
+ this.__synchronizeAttributes();
220
216
  }
221
217
 
222
218
  /** @private */
223
- __updateInvalidState() {
224
- const isValid = this.__getValidateFunction(this.__fieldNode).call(this.__fieldNode);
225
- this.toggleAttribute('invalid', isValid === false);
219
+ __synchronizeAttributes() {
220
+ const field = this.__fieldNode;
221
+ if (!field) {
222
+ this.removeAttribute('required');
223
+ this.removeAttribute('invalid');
224
+ return;
225
+ }
226
+
227
+ this.toggleAttribute('required', field.hasAttribute('required'));
228
+ this.toggleAttribute(
229
+ 'invalid',
230
+ field.hasAttribute('invalid') || (field.matches(':invalid') && this.__isFieldDirty),
231
+ );
226
232
  }
227
233
  };