@vaadin/form-layout 24.7.0-alpha1 → 24.7.0-alpha3

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.0-alpha1",
3
+ "version": "24.7.0-alpha3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -35,23 +35,24 @@
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.7.0-alpha1",
40
- "@vaadin/component-base": "24.7.0-alpha1",
41
- "@vaadin/vaadin-lumo-styles": "24.7.0-alpha1",
42
- "@vaadin/vaadin-material-styles": "24.7.0-alpha1",
43
- "@vaadin/vaadin-themable-mixin": "24.7.0-alpha1"
40
+ "@vaadin/a11y-base": "24.7.0-alpha3",
41
+ "@vaadin/component-base": "24.7.0-alpha3",
42
+ "@vaadin/vaadin-lumo-styles": "24.7.0-alpha3",
43
+ "@vaadin/vaadin-material-styles": "24.7.0-alpha3",
44
+ "@vaadin/vaadin-themable-mixin": "24.7.0-alpha3"
44
45
  },
45
46
  "devDependencies": {
46
- "@vaadin/chai-plugins": "24.7.0-alpha1",
47
- "@vaadin/custom-field": "24.7.0-alpha1",
48
- "@vaadin/testing-helpers": "^1.0.0",
49
- "@vaadin/text-field": "24.7.0-alpha1",
47
+ "@vaadin/chai-plugins": "24.7.0-alpha3",
48
+ "@vaadin/custom-field": "24.7.0-alpha3",
49
+ "@vaadin/testing-helpers": "^1.1.0",
50
+ "@vaadin/text-field": "24.7.0-alpha3",
50
51
  "sinon": "^18.0.0"
51
52
  },
52
53
  "web-types": [
53
54
  "web-types.json",
54
55
  "web-types.lit.json"
55
56
  ],
56
- "gitHead": "04be941c9a7b659871c97f31b9cc3ffd7528087b"
57
+ "gitHead": "dd5cfad6c9b54e676f5b10dffba2233775378f40"
57
58
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2024 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,192 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2024 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
+ /**
10
+ * @polymerMixin
11
+ */
12
+ export const FormItemMixin = (superClass) =>
13
+ class extends superClass {
14
+ constructor() {
15
+ super();
16
+ this.__updateInvalidState = this.__updateInvalidState.bind(this);
17
+
18
+ /**
19
+ * An observer for a field node to reflect its `required` and `invalid` attributes to the component.
20
+ *
21
+ * @type {MutationObserver}
22
+ * @private
23
+ */
24
+ this.__fieldNodeObserver = new MutationObserver(() => this.__updateRequiredState(this.__fieldNode.required));
25
+
26
+ /**
27
+ * The first label node in the label slot.
28
+ *
29
+ * @type {HTMLElement | null}
30
+ * @private
31
+ */
32
+ this.__labelNode = null;
33
+
34
+ /**
35
+ * The first field node in the content slot.
36
+ *
37
+ * An element is considered a field when it has the `checkValidity` or `validate` method.
38
+ *
39
+ * @type {HTMLElement | null}
40
+ * @private
41
+ */
42
+ this.__fieldNode = null;
43
+ }
44
+
45
+ /**
46
+ * Returns a target element to add ARIA attributes to for a field.
47
+ *
48
+ * - For Vaadin field components, the method returns an element
49
+ * obtained through the `ariaTarget` property defined in `FieldMixin`.
50
+ * - In other cases, the method returns the field element itself.
51
+ *
52
+ * @param {HTMLElement} field
53
+ * @protected
54
+ */
55
+ _getFieldAriaTarget(field) {
56
+ return field.ariaTarget || field;
57
+ }
58
+
59
+ /**
60
+ * Links the label to a field by adding the label id to
61
+ * the `aria-labelledby` attribute of the field's ARIA target element.
62
+ *
63
+ * @param {HTMLElement} field
64
+ * @private
65
+ */
66
+ __linkLabelToField(field) {
67
+ addValueToAttribute(this._getFieldAriaTarget(field), 'aria-labelledby', this.__labelId);
68
+ }
69
+
70
+ /**
71
+ * Unlinks the label from a field by removing the label id from
72
+ * the `aria-labelledby` attribute of the field's ARIA target element.
73
+ *
74
+ * @param {HTMLElement} field
75
+ * @private
76
+ */
77
+ __unlinkLabelFromField(field) {
78
+ removeValueFromAttribute(this._getFieldAriaTarget(field), 'aria-labelledby', this.__labelId);
79
+ }
80
+
81
+ /** @private */
82
+ __onLabelClick() {
83
+ const fieldNode = this.__fieldNode;
84
+ if (fieldNode) {
85
+ fieldNode.focus();
86
+ fieldNode.click();
87
+ }
88
+ }
89
+
90
+ /** @private */
91
+ __getValidateFunction(field) {
92
+ return field.validate || field.checkValidity;
93
+ }
94
+
95
+ /**
96
+ * A `slotchange` event handler for the label slot.
97
+ *
98
+ * - Ensures the label id is only assigned to the first label node.
99
+ * - Ensures the label node is linked to the first field node via the `aria-labelledby` attribute
100
+ * if both nodes are provided, and unlinked otherwise.
101
+ *
102
+ * @private
103
+ */
104
+ __onLabelSlotChange() {
105
+ if (this.__labelNode) {
106
+ this.__labelNode = null;
107
+
108
+ if (this.__fieldNode) {
109
+ this.__unlinkLabelFromField(this.__fieldNode);
110
+ }
111
+ }
112
+
113
+ const newLabelNode = this.$.labelSlot.assignedElements()[0];
114
+ if (newLabelNode) {
115
+ this.__labelNode = newLabelNode;
116
+
117
+ if (this.__labelNode.id) {
118
+ // The new label node already has an id. Let's use it.
119
+ this.__labelId = this.__labelNode.id;
120
+ } else {
121
+ // The new label node doesn't have an id yet. Generate a unique one.
122
+ this.__labelId = `label-${this.localName}-${generateUniqueId()}`;
123
+ this.__labelNode.id = this.__labelId;
124
+ }
125
+
126
+ if (this.__fieldNode) {
127
+ this.__linkLabelToField(this.__fieldNode);
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * A `slotchange` event handler for the content slot.
134
+ *
135
+ * - Ensures the label node is only linked to the first field node via the `aria-labelledby` attribute.
136
+ * - Sets up an observer for the `required` attribute changes on the first field
137
+ * to reflect the attribute on the component. Ensures the observer is disconnected from the field
138
+ * as soon as it is removed or replaced by another one.
139
+ *
140
+ * @private
141
+ */
142
+ __onContentSlotChange() {
143
+ if (this.__fieldNode) {
144
+ // Discard the old field
145
+ this.__unlinkLabelFromField(this.__fieldNode);
146
+ this.__updateRequiredState(false);
147
+ this.__fieldNodeObserver.disconnect();
148
+ this.__fieldNode = null;
149
+ }
150
+
151
+ const fieldNodes = this.$.contentSlot.assignedElements();
152
+ if (fieldNodes.length > 1) {
153
+ console.warn(
154
+ `WARNING: Since Vaadin 23, placing multiple fields directly to a <vaadin-form-item> is deprecated.
155
+ Please wrap fields with a <vaadin-custom-field> instead.`,
156
+ );
157
+ }
158
+
159
+ const newFieldNode = fieldNodes.find((field) => {
160
+ return !!this.__getValidateFunction(field);
161
+ });
162
+ if (newFieldNode) {
163
+ this.__fieldNode = newFieldNode;
164
+ this.__updateRequiredState(this.__fieldNode.required);
165
+ this.__fieldNodeObserver.observe(this.__fieldNode, { attributes: true, attributeFilter: ['required'] });
166
+
167
+ if (this.__labelNode) {
168
+ this.__linkLabelToField(this.__fieldNode);
169
+ }
170
+ }
171
+ }
172
+
173
+ /** @private */
174
+ __updateRequiredState(required) {
175
+ if (required) {
176
+ this.setAttribute('required', '');
177
+ this.__fieldNode.addEventListener('blur', this.__updateInvalidState);
178
+ this.__fieldNode.addEventListener('change', this.__updateInvalidState);
179
+ } else {
180
+ this.removeAttribute('invalid');
181
+ this.removeAttribute('required');
182
+ this.__fieldNode.removeEventListener('blur', this.__updateInvalidState);
183
+ this.__fieldNode.removeEventListener('change', this.__updateInvalidState);
184
+ }
185
+ }
186
+
187
+ /** @private */
188
+ __updateInvalidState() {
189
+ const isValid = this.__getValidateFunction(this.__fieldNode).call(this.__fieldNode);
190
+ this.toggleAttribute('invalid', isValid === false);
191
+ }
192
+ };
@@ -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 { 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
@@ -84,16 +85,7 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
84
85
  *
85
86
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
86
87
  */
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
- }
88
+ declare class FormItem extends FormItemMixin(ThemableMixin(HTMLElement)) {}
97
89
 
98
90
  declare global {
99
91
  interface HTMLElementTagNameMap {
@@ -5,9 +5,11 @@
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
@@ -90,52 +92,16 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
90
92
  *
91
93
  * @customElement
92
94
  * @extends HTMLElement
95
+ * @mixes FormItemMixin
93
96
  * @mixes ThemableMixin
94
97
  */
95
- class FormItem extends ThemableMixin(PolymerElement) {
98
+ class FormItem extends FormItemMixin(ThemableMixin(PolymerElement)) {
99
+ static get is() {
100
+ return 'vaadin-form-item';
101
+ }
102
+
96
103
  static get template() {
97
104
  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
105
  <div id="label" part="label" on-click="__onLabelClick">
140
106
  <slot name="label" id="labelSlot" on-slotchange="__onLabelSlotChange"></slot>
141
107
  <span part="required-indicator" aria-hidden="true"></span>
@@ -146,189 +112,6 @@ class FormItem extends ThemableMixin(PolymerElement) {
146
112
  </div>
147
113
  `;
148
114
  }
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
115
  }
333
116
 
334
117
  defineCustomElement(FormItem);
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2024 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
+ import type { ResizeMixinClass } from '@vaadin/component-base/src/resize-mixin.js';
8
+
9
+ export type FormLayoutLabelsPosition = 'aside' | 'top';
10
+
11
+ export type FormLayoutResponsiveStep = {
12
+ minWidth?: string | 0;
13
+ columns: number;
14
+ labelsPosition?: FormLayoutLabelsPosition;
15
+ };
16
+
17
+ /**
18
+ * A mixin providing common form-layout functionality.
19
+ */
20
+ export declare function FormLayoutMixin<T extends Constructor<HTMLElement>>(
21
+ base: T,
22
+ ): Constructor<ResizeMixinClass> & Constructor<FormLayoutMixinClass> & T;
23
+
24
+ export declare class FormLayoutMixinClass {
25
+ /**
26
+ * Allows specifying a responsive behavior with the number of columns
27
+ * and the label position depending on the layout width.
28
+ *
29
+ * Format: array of objects, each object defines one responsive step
30
+ * with `minWidth` CSS length, `columns` number, and optional
31
+ * `labelsPosition` string of `"aside"` or `"top"`. At least one item is required.
32
+ *
33
+ * #### Examples
34
+ *
35
+ * ```javascript
36
+ * formLayout.responsiveSteps = [{columns: 1}];
37
+ * // The layout is always a single column, labels aside.
38
+ * ```
39
+ *
40
+ * ```javascript
41
+ * formLayout.responsiveSteps = [
42
+ * {minWidth: 0, columns: 1},
43
+ * {minWidth: '40em', columns: 2}
44
+ * ];
45
+ * // Sets two responsive steps:
46
+ * // 1. When the layout width is < 40em, one column, labels aside.
47
+ * // 2. Width >= 40em, two columns, labels aside.
48
+ * ```
49
+ *
50
+ * ```javascript
51
+ * formLayout.responsiveSteps = [
52
+ * {minWidth: 0, columns: 1, labelsPosition: 'top'},
53
+ * {minWidth: '20em', columns: 1},
54
+ * {minWidth: '40em', columns: 2}
55
+ * ];
56
+ * // Default value. Three responsive steps:
57
+ * // 1. Width < 20em, one column, labels on top.
58
+ * // 2. 20em <= width < 40em, one column, labels aside.
59
+ * // 3. Width >= 40em, two columns, labels aside.
60
+ * ```
61
+ */
62
+ responsiveSteps: FormLayoutResponsiveStep[];
63
+
64
+ /**
65
+ * Update the layout.
66
+ */
67
+ protected _updateLayout(): void;
68
+ }