@vaadin/form-layout 22.0.0-rc1 → 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": "
|
|
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": "
|
|
38
|
-
"@vaadin/vaadin-lumo-styles": "
|
|
39
|
-
"@vaadin/vaadin-material-styles": "
|
|
40
|
-
"@vaadin/vaadin-themable-mixin": "
|
|
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": "
|
|
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,12 +146,13 @@ 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>
|
|
151
|
+
<span part="required-indicator" aria-hidden="true"></span>
|
|
150
152
|
</div>
|
|
151
153
|
<div id="spacing"></div>
|
|
152
154
|
<div id="content">
|
|
153
|
-
<slot id="contentSlot"></slot>
|
|
155
|
+
<slot id="contentSlot" on-slotchange="__onContentSlotChange"></slot>
|
|
154
156
|
</div>
|
|
155
157
|
`;
|
|
156
158
|
}
|
|
@@ -159,17 +161,173 @@ class FormItem extends ThemableMixin(PolymerElement) {
|
|
|
159
161
|
return 'vaadin-form-item';
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
constructor() {
|
|
165
|
+
super();
|
|
166
|
+
this.__updateInvalidState = this.__updateInvalidState.bind(this);
|
|
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);
|
|
233
|
+
}
|
|
234
|
+
|
|
162
235
|
/** @private */
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
(
|
|
167
|
-
|
|
168
|
-
if (firstContentElementChild) {
|
|
169
|
-
firstContentElementChild.focus();
|
|
170
|
-
firstContentElementChild.click();
|
|
236
|
+
__onLabelClick() {
|
|
237
|
+
const fieldNode = this.__fieldNode;
|
|
238
|
+
if (fieldNode) {
|
|
239
|
+
fieldNode.focus();
|
|
240
|
+
fieldNode.click();
|
|
171
241
|
}
|
|
172
242
|
}
|
|
243
|
+
|
|
244
|
+
/** @private */
|
|
245
|
+
__getValidateFunction(field) {
|
|
246
|
+
return field.validate || field.checkValidity;
|
|
247
|
+
}
|
|
248
|
+
|
|
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
|
+
*/
|
|
289
|
+
__onContentSlotChange() {
|
|
290
|
+
if (this.__fieldNode) {
|
|
291
|
+
// Discard the old field
|
|
292
|
+
this.__unlinkLabelFromField(this.__fieldNode);
|
|
293
|
+
this.__updateRequiredState(false);
|
|
294
|
+
this.__fieldNodeObserver.disconnect();
|
|
295
|
+
this.__fieldNode = null;
|
|
296
|
+
}
|
|
297
|
+
|
|
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
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** @private */
|
|
313
|
+
__updateRequiredState(required) {
|
|
314
|
+
if (required) {
|
|
315
|
+
this.setAttribute('required', '');
|
|
316
|
+
this.__fieldNode.addEventListener('blur', this.__updateInvalidState);
|
|
317
|
+
this.__fieldNode.addEventListener('change', this.__updateInvalidState);
|
|
318
|
+
} else {
|
|
319
|
+
this.removeAttribute('invalid');
|
|
320
|
+
this.removeAttribute('required');
|
|
321
|
+
this.__fieldNode.removeEventListener('blur', this.__updateInvalidState);
|
|
322
|
+
this.__fieldNode.removeEventListener('change', this.__updateInvalidState);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** @private */
|
|
327
|
+
__updateInvalidState() {
|
|
328
|
+
const isValid = this.__getValidateFunction(this.__fieldNode).call(this.__fieldNode);
|
|
329
|
+
this.toggleAttribute('invalid', isValid === false);
|
|
330
|
+
}
|
|
173
331
|
}
|
|
174
332
|
|
|
175
333
|
customElements.define(FormItem.is, FormItem);
|
|
@@ -23,6 +23,24 @@ registerStyles(
|
|
|
23
23
|
transition: color 0.4s;
|
|
24
24
|
line-height: 1.333;
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
[part='required-indicator']::after {
|
|
28
|
+
content: var(--lumo-required-field-indicator, '•');
|
|
29
|
+
transition: opacity 0.2s;
|
|
30
|
+
opacity: 0;
|
|
31
|
+
color: var(--lumo-required-field-indicator-color, var(--lumo-primary-text-color));
|
|
32
|
+
position: relative;
|
|
33
|
+
width: 1em;
|
|
34
|
+
text-align: center;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
:host([required]) [part='required-indicator']::after {
|
|
38
|
+
opacity: 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
:host([invalid]) [part='required-indicator']::after {
|
|
42
|
+
color: var(--lumo-required-field-indicator-color, var(--lumo-error-text-color));
|
|
43
|
+
}
|
|
26
44
|
`,
|
|
27
45
|
{ moduleId: 'lumo-form-item' }
|
|
28
46
|
);
|
|
@@ -14,6 +14,15 @@ registerStyles(
|
|
|
14
14
|
margin-top: 16px;
|
|
15
15
|
margin-bottom: 8px;
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
:host([required]) [part='required-indicator']::after {
|
|
19
|
+
content: ' *';
|
|
20
|
+
color: inherit;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
:host([invalid]) [part='label'] {
|
|
24
|
+
color: var(--material-error-text-color);
|
|
25
|
+
}
|
|
17
26
|
`,
|
|
18
27
|
{ moduleId: 'material-form-item' }
|
|
19
28
|
);
|