@vaadin/form-layout 24.6.5 → 24.7.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.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2025 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
+ }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 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 { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.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
+ /**
17
+ * @polymerMixin
18
+ * @mixes ResizeMixin
19
+ */
20
+ export const FormLayoutMixin = (superClass) =>
21
+ class extends ResizeMixin(superClass) {
22
+ static get properties() {
23
+ return {
24
+ /**
25
+ * @typedef FormLayoutResponsiveStep
26
+ * @type {object}
27
+ * @property {string} minWidth - The threshold value for this step in CSS length units.
28
+ * @property {number} columns - Number of columns. Only natural numbers are valid.
29
+ * @property {string} labelsPosition - Labels position option, valid values: `"aside"` (default), `"top"`.
30
+ */
31
+
32
+ /**
33
+ * Allows specifying a responsive behavior with the number of columns
34
+ * and the label position depending on the layout width.
35
+ *
36
+ * Format: array of objects, each object defines one responsive step
37
+ * with `minWidth` CSS length, `columns` number, and optional
38
+ * `labelsPosition` string of `"aside"` or `"top"`. At least one item is required.
39
+ *
40
+ * #### Examples
41
+ *
42
+ * ```javascript
43
+ * formLayout.responsiveSteps = [{columns: 1}];
44
+ * // The layout is always a single column, labels aside.
45
+ * ```
46
+ *
47
+ * ```javascript
48
+ * formLayout.responsiveSteps = [
49
+ * {minWidth: 0, columns: 1},
50
+ * {minWidth: '40em', columns: 2}
51
+ * ];
52
+ * // Sets two responsive steps:
53
+ * // 1. When the layout width is < 40em, one column, labels aside.
54
+ * // 2. Width >= 40em, two columns, labels aside.
55
+ * ```
56
+ *
57
+ * ```javascript
58
+ * formLayout.responsiveSteps = [
59
+ * {minWidth: 0, columns: 1, labelsPosition: 'top'},
60
+ * {minWidth: '20em', columns: 1},
61
+ * {minWidth: '40em', columns: 2}
62
+ * ];
63
+ * // Default value. Three responsive steps:
64
+ * // 1. Width < 20em, one column, labels on top.
65
+ * // 2. 20em <= width < 40em, one column, labels aside.
66
+ * // 3. Width >= 40em, two columns, labels aside.
67
+ * ```
68
+ *
69
+ * @type {!Array<!FormLayoutResponsiveStep>}
70
+ */
71
+ responsiveSteps: {
72
+ type: Array,
73
+ value() {
74
+ return [
75
+ { minWidth: 0, columns: 1, labelsPosition: 'top' },
76
+ { minWidth: '20em', columns: 1 },
77
+ { minWidth: '40em', columns: 2 },
78
+ ];
79
+ },
80
+ observer: '_responsiveStepsChanged',
81
+ sync: true,
82
+ },
83
+
84
+ /**
85
+ * Current number of columns in the layout
86
+ * @private
87
+ */
88
+ _columnCount: {
89
+ type: Number,
90
+ sync: true,
91
+ },
92
+
93
+ /**
94
+ * Indicates that labels are on top
95
+ * @private
96
+ */
97
+ _labelsOnTop: {
98
+ type: Boolean,
99
+ sync: true,
100
+ },
101
+ };
102
+ }
103
+
104
+ static get observers() {
105
+ return ['_invokeUpdateLayout(_columnCount, _labelsOnTop)'];
106
+ }
107
+
108
+ /** @protected */
109
+ connectedCallback() {
110
+ super.connectedCallback();
111
+
112
+ // Set up an observer to update layout when new children are added or removed.
113
+ this.__childrenObserver = new MutationObserver(() => this._updateLayout());
114
+ this.__childrenObserver.observe(this, { childList: true });
115
+
116
+ // Set up an observer to update layout when children's attributes change.
117
+ this.__childrenAttributesObserver = new MutationObserver((mutations) => {
118
+ if (mutations.some((mutation) => mutation.target.parentElement === this)) {
119
+ this._updateLayout();
120
+ }
121
+ });
122
+ this.__childrenAttributesObserver.observe(this, {
123
+ subtree: true,
124
+ attributes: true,
125
+ attributeFilter: ['colspan', 'data-colspan', 'hidden'],
126
+ });
127
+
128
+ requestAnimationFrame(() => this._selectResponsiveStep());
129
+ requestAnimationFrame(() => this._updateLayout());
130
+ }
131
+
132
+ /** @protected */
133
+ disconnectedCallback() {
134
+ super.disconnectedCallback();
135
+
136
+ this.__childrenObserver.disconnect();
137
+ this.__childrenAttributesObserver.disconnect();
138
+ }
139
+
140
+ /** @private */
141
+ _naturalNumberOrOne(n) {
142
+ if (typeof n === 'number' && n >= 1 && n < Infinity) {
143
+ return Math.floor(n);
144
+ }
145
+ return 1;
146
+ }
147
+
148
+ /** @private */
149
+ _responsiveStepsChanged(responsiveSteps, oldResponsiveSteps) {
150
+ try {
151
+ if (!Array.isArray(responsiveSteps)) {
152
+ throw new Error('Invalid "responsiveSteps" type, an Array is required.');
153
+ }
154
+
155
+ if (responsiveSteps.length < 1) {
156
+ throw new Error('Invalid empty "responsiveSteps" array, at least one item is required.');
157
+ }
158
+
159
+ responsiveSteps.forEach((step) => {
160
+ if (this._naturalNumberOrOne(step.columns) !== step.columns) {
161
+ throw new Error(`Invalid 'columns' value of ${step.columns}, a natural number is required.`);
162
+ }
163
+
164
+ if (step.minWidth !== undefined && !isValidCSSLength(step.minWidth)) {
165
+ throw new Error(`Invalid 'minWidth' value of ${step.minWidth}, a valid CSS length required.`);
166
+ }
167
+
168
+ if (step.labelsPosition !== undefined && ['aside', 'top'].indexOf(step.labelsPosition) === -1) {
169
+ throw new Error(
170
+ `Invalid 'labelsPosition' value of ${step.labelsPosition}, 'aside' or 'top' string is required.`,
171
+ );
172
+ }
173
+ });
174
+ } catch (e) {
175
+ if (oldResponsiveSteps && oldResponsiveSteps !== responsiveSteps) {
176
+ console.warn(`${e.message} Using previously set 'responsiveSteps' instead.`);
177
+ this.responsiveSteps = oldResponsiveSteps;
178
+ } else {
179
+ console.warn(`${e.message} Using default 'responsiveSteps' instead.`);
180
+ this.responsiveSteps = [
181
+ { minWidth: 0, columns: 1, labelsPosition: 'top' },
182
+ { minWidth: '20em', columns: 1 },
183
+ { minWidth: '40em', columns: 2 },
184
+ ];
185
+ }
186
+ }
187
+
188
+ this._selectResponsiveStep();
189
+ }
190
+
191
+ /** @private */
192
+ _selectResponsiveStep() {
193
+ // Iterate through responsiveSteps and choose the step
194
+ let selectedStep;
195
+ const tmpStyleProp = 'background-position';
196
+ this.responsiveSteps.forEach((step) => {
197
+ // Convert minWidth to px units for comparison
198
+ this.$.layout.style.setProperty(tmpStyleProp, step.minWidth);
199
+ const stepMinWidthPx = parseFloat(getComputedStyle(this.$.layout).getPropertyValue(tmpStyleProp));
200
+
201
+ // Compare step min-width with the host width, select the passed step
202
+ if (stepMinWidthPx <= this.offsetWidth) {
203
+ selectedStep = step;
204
+ }
205
+ });
206
+ this.$.layout.style.removeProperty(tmpStyleProp);
207
+
208
+ // Sometimes converting units is not possible, e.g, when element is
209
+ // not connected. Then the `selectedStep` stays `undefined`.
210
+ if (selectedStep) {
211
+ // Apply the chosen responsive step's properties
212
+ this._columnCount = selectedStep.columns;
213
+ this._labelsOnTop = selectedStep.labelsPosition === 'top';
214
+ }
215
+ }
216
+
217
+ /** @private */
218
+ _invokeUpdateLayout() {
219
+ this._updateLayout();
220
+ }
221
+
222
+ /**
223
+ * Update the layout.
224
+ * @protected
225
+ */
226
+ _updateLayout() {
227
+ // Do not update layout when invisible
228
+ if (isElementHidden(this)) {
229
+ return;
230
+ }
231
+
232
+ /*
233
+ The item width formula:
234
+
235
+ itemWidth = colspan / columnCount * 100% - columnSpacing
236
+
237
+ We have to subtract columnSpacing, because the column spacing space is taken
238
+ by item margins of 1/2 * spacing on both sides
239
+ */
240
+
241
+ const style = getComputedStyle(this);
242
+ const columnSpacing = style.getPropertyValue('--vaadin-form-layout-column-spacing');
243
+
244
+ const direction = style.direction;
245
+ const marginStartProp = `margin-${direction === 'ltr' ? 'left' : 'right'}`;
246
+ const marginEndProp = `margin-${direction === 'ltr' ? 'right' : 'left'}`;
247
+
248
+ const containerWidth = this.offsetWidth;
249
+
250
+ let col = 0;
251
+ Array.from(this.children)
252
+ .filter((child) => child.localName === 'br' || getComputedStyle(child).display !== 'none')
253
+ .forEach((child, index, children) => {
254
+ if (child.localName === 'br') {
255
+ // Reset column count on line break
256
+ col = 0;
257
+ return;
258
+ }
259
+
260
+ const attrColspan = child.getAttribute('colspan') || child.getAttribute('data-colspan');
261
+ let colspan;
262
+ colspan = this._naturalNumberOrOne(parseFloat(attrColspan));
263
+
264
+ // Never span further than the number of columns
265
+ colspan = Math.min(colspan, this._columnCount);
266
+
267
+ const childRatio = colspan / this._columnCount;
268
+ child.style.width = `calc(${childRatio * 100}% - ${1 - childRatio} * ${columnSpacing})`;
269
+
270
+ if (col + colspan > this._columnCount) {
271
+ // Too big to fit on this row, let's wrap it
272
+ col = 0;
273
+ }
274
+
275
+ // At the start edge
276
+ if (col === 0) {
277
+ child.style.setProperty(marginStartProp, '0px');
278
+ } else {
279
+ child.style.removeProperty(marginStartProp);
280
+ }
281
+
282
+ const nextIndex = index + 1;
283
+ const nextLineBreak = nextIndex < children.length && children[nextIndex].localName === 'br';
284
+
285
+ // At the end edge
286
+ if (col + colspan === this._columnCount) {
287
+ child.style.setProperty(marginEndProp, '0px');
288
+ } else if (nextLineBreak) {
289
+ const colspanRatio = (this._columnCount - col - colspan) / this._columnCount;
290
+ child.style.setProperty(
291
+ marginEndProp,
292
+ `calc(${colspanRatio * containerWidth}px + ${colspanRatio} * ${columnSpacing})`,
293
+ );
294
+ } else {
295
+ child.style.removeProperty(marginEndProp);
296
+ }
297
+
298
+ // Move the column counter
299
+ col = (col + colspan) % this._columnCount;
300
+
301
+ if (child.localName === 'vaadin-form-item') {
302
+ if (this._labelsOnTop) {
303
+ if (child.getAttribute('label-position') !== 'top') {
304
+ child.__useLayoutLabelPosition = true;
305
+ child.setAttribute('label-position', 'top');
306
+ }
307
+ } else if (child.__useLayoutLabelPosition) {
308
+ delete child.__useLayoutLabelPosition;
309
+ child.removeAttribute('label-position');
310
+ }
311
+ }
312
+ });
313
+ }
314
+
315
+ /**
316
+ * @protected
317
+ * @override
318
+ */
319
+ _onResize(contentRect) {
320
+ if (contentRect.width === 0 && contentRect.height === 0) {
321
+ this.$.layout.style.opacity = '0';
322
+ return;
323
+ }
324
+
325
+ this._selectResponsiveStep();
326
+ this._updateLayout();
327
+
328
+ this.$.layout.style.opacity = '';
329
+ }
330
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2018 - 2025 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import type { CSSResult } from 'lit';
7
+
8
+ export const formLayoutStyles: CSSResult;
9
+
10
+ export const formItemStyles: CSSResult;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2018 - 2025 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { css } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
7
+
8
+ export const formLayoutStyles = css`
9
+ :host {
10
+ /* Default values */
11
+ --vaadin-form-layout-row-spacing: 1em;
12
+ --vaadin-form-layout-column-spacing: 2em;
13
+ --vaadin-form-layout-label-width: 8em;
14
+ --vaadin-form-layout-label-spacing: 1em;
15
+
16
+ display: block;
17
+ max-width: 100%;
18
+ align-self: stretch;
19
+ }
20
+
21
+ :host([hidden]) {
22
+ display: none !important;
23
+ }
24
+
25
+ #layout {
26
+ display: flex;
27
+
28
+ align-items: baseline; /* default \`stretch\` is not appropriate */
29
+
30
+ flex-wrap: wrap; /* the items should wrap */
31
+ }
32
+
33
+ #layout ::slotted(*) {
34
+ /* Items should neither grow nor shrink. */
35
+ flex-grow: 0;
36
+ flex-shrink: 0;
37
+
38
+ /* Margins make spacing between the columns */
39
+ margin-left: calc(0.5 * var(--vaadin-form-layout-column-spacing));
40
+ margin-right: calc(0.5 * var(--vaadin-form-layout-column-spacing));
41
+ }
42
+
43
+ #layout ::slotted(br) {
44
+ display: none;
45
+ }
46
+ `;
47
+
48
+ export const formItemStyles = css`
49
+ :host {
50
+ display: inline-flex;
51
+ flex-direction: row;
52
+ align-items: baseline;
53
+ margin: calc(0.5 * var(--vaadin-form-item-row-spacing, var(--vaadin-form-layout-row-spacing, 1em))) 0;
54
+ }
55
+
56
+ :host([label-position='top']) {
57
+ flex-direction: column;
58
+ align-items: stretch;
59
+ }
60
+
61
+ :host([hidden]) {
62
+ display: none !important;
63
+ }
64
+
65
+ #label {
66
+ width: var(--vaadin-form-item-label-width, var(--vaadin-form-layout-label-width, 8em));
67
+ flex: 0 0 auto;
68
+ }
69
+
70
+ :host([label-position='top']) #label {
71
+ width: auto;
72
+ }
73
+
74
+ #spacing {
75
+ width: var(--vaadin-form-item-label-spacing, var(--vaadin-form-layout-label-spacing, 1em));
76
+ flex: 0 0 auto;
77
+ }
78
+
79
+ #content {
80
+ flex: 1 1 auto;
81
+ }
82
+
83
+ #content ::slotted(.full-width) {
84
+ box-sizing: border-box;
85
+ width: 100%;
86
+ min-width: 0;
87
+ }
88
+ `;
@@ -1,19 +1,13 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2017 - 2024 Vaadin Ltd.
3
+ * Copyright (c) 2017 - 2025 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
7
- import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
8
7
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
8
+ import { FormLayoutMixin } from './vaadin-form-layout-mixin.js';
9
9
 
10
- export type FormLayoutLabelsPosition = 'aside' | 'top';
11
-
12
- export type FormLayoutResponsiveStep = {
13
- minWidth?: string | 0;
14
- columns: number;
15
- labelsPosition?: FormLayoutLabelsPosition;
16
- };
10
+ export * from './vaadin-form-layout-mixin.js';
17
11
 
18
12
  /**
19
13
  * `<vaadin-form-layout>` is a Web Component providing configurable responsive
@@ -104,52 +98,9 @@ export type FormLayoutResponsiveStep = {
104
98
  * Custom CSS property | Description | Default
105
99
  * ---|---|---
106
100
  * `--vaadin-form-layout-column-spacing` | Length of the spacing between columns | `2em`
101
+ * `--vaadin-form-layout-row-spacing` | Length of the spacing between rows | `1em`
107
102
  */
108
- declare class FormLayout extends ResizeMixin(ElementMixin(ThemableMixin(HTMLElement))) {
109
- /**
110
- * Allows specifying a responsive behavior with the number of columns
111
- * and the label position depending on the layout width.
112
- *
113
- * Format: array of objects, each object defines one responsive step
114
- * with `minWidth` CSS length, `columns` number, and optional
115
- * `labelsPosition` string of `"aside"` or `"top"`. At least one item is required.
116
- *
117
- * #### Examples
118
- *
119
- * ```javascript
120
- * formLayout.responsiveSteps = [{columns: 1}];
121
- * // The layout is always a single column, labels aside.
122
- * ```
123
- *
124
- * ```javascript
125
- * formLayout.responsiveSteps = [
126
- * {minWidth: 0, columns: 1},
127
- * {minWidth: '40em', columns: 2}
128
- * ];
129
- * // Sets two responsive steps:
130
- * // 1. When the layout width is < 40em, one column, labels aside.
131
- * // 2. Width >= 40em, two columns, labels aside.
132
- * ```
133
- *
134
- * ```javascript
135
- * formLayout.responsiveSteps = [
136
- * {minWidth: 0, columns: 1, labelsPosition: 'top'},
137
- * {minWidth: '20em', columns: 1},
138
- * {minWidth: '40em', columns: 2}
139
- * ];
140
- * // Default value. Three responsive steps:
141
- * // 1. Width < 20em, one column, labels on top.
142
- * // 2. 20em <= width < 40em, one column, labels aside.
143
- * // 3. Width >= 40em, two columns, labels aside.
144
- * ```
145
- */
146
- responsiveSteps: FormLayoutResponsiveStep[];
147
-
148
- /**
149
- * Update the layout.
150
- */
151
- protected _updateLayout(): void;
152
- }
103
+ declare class FormLayout extends FormLayoutMixin(ElementMixin(ThemableMixin(HTMLElement))) {}
153
104
 
154
105
  declare global {
155
106
  interface HTMLElementTagNameMap {