@vaadin/form-layout 23.0.0-alpha1 → 23.0.0-alpha2

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": "23.0.0-alpha1",
3
+ "version": "23.0.0-alpha2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -34,16 +34,17 @@
34
34
  "dependencies": {
35
35
  "@polymer/iron-resizable-behavior": "^3.0.0",
36
36
  "@polymer/polymer": "^3.0.0",
37
- "@vaadin/component-base": "23.0.0-alpha1",
38
- "@vaadin/vaadin-lumo-styles": "23.0.0-alpha1",
39
- "@vaadin/vaadin-material-styles": "23.0.0-alpha1",
40
- "@vaadin/vaadin-themable-mixin": "23.0.0-alpha1"
37
+ "@vaadin/component-base": "23.0.0-alpha2",
38
+ "@vaadin/vaadin-lumo-styles": "23.0.0-alpha2",
39
+ "@vaadin/vaadin-material-styles": "23.0.0-alpha2",
40
+ "@vaadin/vaadin-themable-mixin": "23.0.0-alpha2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@esm-bundle/chai": "^4.3.4",
44
+ "@vaadin/custom-field": "23.0.0-alpha2",
44
45
  "@vaadin/testing-helpers": "^0.3.2",
45
- "@vaadin/text-field": "23.0.0-alpha1",
46
+ "@vaadin/text-field": "23.0.0-alpha2",
46
47
  "sinon": "^9.2.1"
47
48
  },
48
- "gitHead": "fbcb07328fdf88260e3b461088d207426b21c710"
49
+ "gitHead": "070f586dead02ca41b66717820c647f48bf1665f"
49
50
  }
@@ -97,7 +97,16 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
97
97
  *
98
98
  * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
99
99
  */
100
- declare class FormItem extends ThemableMixin(HTMLElement) {}
100
+ declare class FormItem extends ThemableMixin(HTMLElement) {
101
+ /**
102
+ * Returns a target element to add ARIA attributes to for a field.
103
+ *
104
+ * - For Vaadin field components, the method returns an element
105
+ * obtained through the `ariaTarget` property defined in `FieldMixin`.
106
+ * - In other cases, the method returns the field element itself.
107
+ */
108
+ protected _getFieldAriaTarget(field: HTMLElement): HTMLElement;
109
+ }
101
110
 
102
111
  declare global {
103
112
  interface HTMLElementTagNameMap {
@@ -4,6 +4,7 @@
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
+ import { addValueToAttribute, removeValueFromAttribute } from '@vaadin/field-base/src/utils.js';
7
8
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
8
9
 
9
10
  /**
@@ -145,8 +146,8 @@ class FormItem extends ThemableMixin(PolymerElement) {
145
146
  min-width: 0;
146
147
  }
147
148
  </style>
148
- <div id="label" part="label" on-click="_onLabelClick">
149
- <slot name="label" id="labelSlot"></slot>
149
+ <div id="label" part="label" on-click="__onLabelClick">
150
+ <slot name="label" id="labelSlot" on-slotchange="__onLabelSlotChange"></slot>
150
151
  <span part="required-indicator" aria-hidden="true"></span>
151
152
  </div>
152
153
  <div id="spacing"></div>
@@ -163,15 +164,80 @@ class FormItem extends ThemableMixin(PolymerElement) {
163
164
  constructor() {
164
165
  super();
165
166
  this.__updateInvalidState = this.__updateInvalidState.bind(this);
166
- this.__contentFieldObserver = new MutationObserver(() => this.__updateRequiredState(this.__contentField.required));
167
+
168
+ /**
169
+ * An observer for a field node to reflect its `required` and `invalid` attributes to the component.
170
+ *
171
+ * @type {MutationObserver}
172
+ * @private
173
+ */
174
+ this.__fieldNodeObserver = new MutationObserver(() => this.__updateRequiredState(this.__fieldNode.required));
175
+
176
+ /**
177
+ * The first label node in the label slot.
178
+ *
179
+ * @type {HTMLElement | null}
180
+ * @private
181
+ */
182
+ this.__labelNode = null;
183
+
184
+ /**
185
+ * The first field node in the content slot.
186
+ *
187
+ * An element is considered a field when it has the `checkValidity` or `validate` method.
188
+ *
189
+ * @type {HTMLElement | null}
190
+ * @private
191
+ */
192
+ this.__fieldNode = null;
193
+
194
+ // Ensure every instance has unique ID
195
+ const uniqueId = (FormItem._uniqueLabelId = 1 + FormItem._uniqueLabelId || 0);
196
+ this.__labelId = `label-${this.localName}-${uniqueId}`;
197
+ }
198
+
199
+ /**
200
+ * Returns a target element to add ARIA attributes to for a field.
201
+ *
202
+ * - For Vaadin field components, the method returns an element
203
+ * obtained through the `ariaTarget` property defined in `FieldMixin`.
204
+ * - In other cases, the method returns the field element itself.
205
+ *
206
+ * @param {HTMLElement} field
207
+ * @protected
208
+ */
209
+ _getFieldAriaTarget(field) {
210
+ return field.ariaTarget || field;
211
+ }
212
+
213
+ /**
214
+ * Links the label to a field by adding the label id to
215
+ * the `aria-labelledby` attribute of the field's ARIA target element.
216
+ *
217
+ * @param {HTMLElement} field
218
+ * @private
219
+ */
220
+ __linkLabelToField(field) {
221
+ addValueToAttribute(this._getFieldAriaTarget(field), 'aria-labelledby', this.__labelId);
222
+ }
223
+
224
+ /**
225
+ * Unlinks the label from a field by removing the label id from
226
+ * the `aria-labelledby` attribute of the field's ARIA target element.
227
+ *
228
+ * @param {HTMLElement} field
229
+ * @private
230
+ */
231
+ __unlinkLabelFromField(field) {
232
+ removeValueFromAttribute(this._getFieldAriaTarget(field), 'aria-labelledby', this.__labelId);
167
233
  }
168
234
 
169
235
  /** @private */
170
- _onLabelClick() {
171
- const firstContentElementChild = this.$.contentSlot.assignedElements()[0];
172
- if (firstContentElementChild) {
173
- firstContentElementChild.focus();
174
- firstContentElementChild.click();
236
+ __onLabelClick() {
237
+ const fieldNode = this.__fieldNode;
238
+ if (fieldNode) {
239
+ fieldNode.focus();
240
+ fieldNode.click();
175
241
  }
176
242
  }
177
243
 
@@ -180,21 +246,66 @@ class FormItem extends ThemableMixin(PolymerElement) {
180
246
  return field.validate || field.checkValidity;
181
247
  }
182
248
 
183
- /** @private */
249
+ /**
250
+ * A `slotchange` event handler for the label slot.
251
+ *
252
+ * - Ensures the label id is only assigned to the first label node.
253
+ * - Ensures the label node is linked to the first field node via the `aria-labelledby` attribute
254
+ * if both nodes are provided, and unlinked otherwise.
255
+ *
256
+ * @private
257
+ */
258
+ __onLabelSlotChange() {
259
+ if (this.__labelNode) {
260
+ this.__labelNode.id = '';
261
+ this.__labelNode = null;
262
+
263
+ if (this.__fieldNode) {
264
+ this.__unlinkLabelFromField(this.__fieldNode);
265
+ }
266
+ }
267
+
268
+ const newLabelNode = this.$.labelSlot.assignedElements()[0];
269
+ if (newLabelNode) {
270
+ this.__labelNode = newLabelNode;
271
+ this.__labelNode.id = this.__labelId;
272
+
273
+ if (this.__fieldNode) {
274
+ this.__linkLabelToField(this.__fieldNode);
275
+ }
276
+ }
277
+ }
278
+
279
+ /**
280
+ * A `slotchange` event handler for the content slot.
281
+ *
282
+ * - Ensures the label node is only linked to the first field node via the `aria-labelledby` attribute.
283
+ * - Sets up an observer for the `required` attribute changes on the first field
284
+ * to reflect the attribute on the component. Ensures the observer is disconnected from the field
285
+ * as soon as it is removed or replaced by another one.
286
+ *
287
+ * @private
288
+ */
184
289
  __onContentSlotChange() {
185
- if (this.__contentField) {
290
+ if (this.__fieldNode) {
186
291
  // Discard the old field
292
+ this.__unlinkLabelFromField(this.__fieldNode);
187
293
  this.__updateRequiredState(false);
188
- this.__contentFieldObserver.disconnect();
189
- delete this.__contentField;
294
+ this.__fieldNodeObserver.disconnect();
295
+ this.__fieldNode = null;
190
296
  }
191
297
 
192
- const contentFields = this.$.contentSlot.assignedElements().filter((node) => !!this.__getValidateFunction(node));
193
- if (contentFields.length === 1) {
194
- // There's only one child field
195
- this.__contentField = contentFields[0];
196
- this.__updateRequiredState(this.__contentField.required);
197
- this.__contentFieldObserver.observe(this.__contentField, { attributes: true, attributeFilter: ['required'] });
298
+ const newFieldNode = this.$.contentSlot.assignedElements().find((field) => {
299
+ return !!this.__getValidateFunction(field);
300
+ });
301
+ if (newFieldNode) {
302
+ this.__fieldNode = newFieldNode;
303
+ this.__updateRequiredState(this.__fieldNode.required);
304
+ this.__fieldNodeObserver.observe(this.__fieldNode, { attributes: true, attributeFilter: ['required'] });
305
+
306
+ if (this.__labelNode) {
307
+ this.__linkLabelToField(this.__fieldNode);
308
+ }
198
309
  }
199
310
  }
200
311
 
@@ -202,22 +313,20 @@ class FormItem extends ThemableMixin(PolymerElement) {
202
313
  __updateRequiredState(required) {
203
314
  if (required) {
204
315
  this.setAttribute('required', '');
205
- this.__contentField.addEventListener('blur', this.__updateInvalidState);
206
- this.__contentField.addEventListener('change', this.__updateInvalidState);
316
+ this.__fieldNode.addEventListener('blur', this.__updateInvalidState);
317
+ this.__fieldNode.addEventListener('change', this.__updateInvalidState);
207
318
  } else {
208
319
  this.removeAttribute('invalid');
209
320
  this.removeAttribute('required');
210
- this.__contentField.removeEventListener('blur', this.__updateInvalidState);
211
- this.__contentField.removeEventListener('change', this.__updateInvalidState);
321
+ this.__fieldNode.removeEventListener('blur', this.__updateInvalidState);
322
+ this.__fieldNode.removeEventListener('change', this.__updateInvalidState);
212
323
  }
213
324
  }
214
325
 
215
326
  /** @private */
216
327
  __updateInvalidState() {
217
- this.toggleAttribute(
218
- 'invalid',
219
- this.__getValidateFunction(this.__contentField).call(this.__contentField) === false
220
- );
328
+ const isValid = this.__getValidateFunction(this.__fieldNode).call(this.__fieldNode);
329
+ this.toggleAttribute('invalid', isValid === false);
221
330
  }
222
331
  }
223
332