@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 +8 -7
- package/src/vaadin-form-item.d.ts +10 -1
- package/src/vaadin-form-item.js +135 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/form-layout",
|
|
3
|
-
"version": "23.0.0-
|
|
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-
|
|
38
|
-
"@vaadin/vaadin-lumo-styles": "23.0.0-
|
|
39
|
-
"@vaadin/vaadin-material-styles": "23.0.0-
|
|
40
|
-
"@vaadin/vaadin-themable-mixin": "23.0.0-
|
|
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-
|
|
46
|
+
"@vaadin/text-field": "23.0.0-alpha2",
|
|
46
47
|
"sinon": "^9.2.1"
|
|
47
48
|
},
|
|
48
|
-
"gitHead": "
|
|
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 {
|
package/src/vaadin-form-item.js
CHANGED
|
@@ -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="
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
/**
|
|
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.
|
|
290
|
+
if (this.__fieldNode) {
|
|
186
291
|
// Discard the old field
|
|
292
|
+
this.__unlinkLabelFromField(this.__fieldNode);
|
|
187
293
|
this.__updateRequiredState(false);
|
|
188
|
-
this.
|
|
189
|
-
|
|
294
|
+
this.__fieldNodeObserver.disconnect();
|
|
295
|
+
this.__fieldNode = null;
|
|
190
296
|
}
|
|
191
297
|
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this.
|
|
197
|
-
this.
|
|
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.
|
|
206
|
-
this.
|
|
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.
|
|
211
|
-
this.
|
|
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.
|
|
218
|
-
|
|
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
|
|