@vaadin/form-layout 24.7.2 → 24.8.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.
@@ -4,7 +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 type { Constructor } from '@open-wc/dedupe-mixin';
7
- import type { ResizeMixinClass } from '@vaadin/component-base/src/resize-mixin.js';
7
+ import type { SlotStylesMixinClass } from '@vaadin/component-base/src/slot-styles-mixin.js';
8
8
 
9
9
  export type FormLayoutLabelsPosition = 'aside' | 'top';
10
10
 
@@ -19,7 +19,7 @@ export type FormLayoutResponsiveStep = {
19
19
  */
20
20
  export declare function FormLayoutMixin<T extends Constructor<HTMLElement>>(
21
21
  base: T,
22
- ): Constructor<ResizeMixinClass> & Constructor<FormLayoutMixinClass> & T;
22
+ ): Constructor<FormLayoutMixinClass> & Constructor<SlotStylesMixinClass> & T;
23
23
 
24
24
  export declare class FormLayoutMixinClass {
25
25
  /**
@@ -30,6 +30,14 @@ export declare class FormLayoutMixinClass {
30
30
  * with `minWidth` CSS length, `columns` number, and optional
31
31
  * `labelsPosition` string of `"aside"` or `"top"`. At least one item is required.
32
32
  *
33
+ * NOTE: Responsive steps are ignored in auto-responsive mode, which may be
34
+ * enabled explicitly via the `autoResponsive` property or implicitly
35
+ * if the following feature flag is set:
36
+ *
37
+ * ```
38
+ * window.Vaadin.featureFlags.defaultAutoResponsiveFormLayout = true
39
+ * ```
40
+ *
33
41
  * #### Examples
34
42
  *
35
43
  * ```javascript
@@ -61,6 +69,107 @@ export declare class FormLayoutMixinClass {
61
69
  */
62
70
  responsiveSteps: FormLayoutResponsiveStep[];
63
71
 
72
+ /**
73
+ * When set to `true`, the component automatically creates and adjusts columns based on
74
+ * the container's width. Columns have a fixed width defined by `columnWidth` and their
75
+ * number increases up to the limit set by `maxColumns`. The component dynamically adjusts
76
+ * the number of columns as the container size changes. When this mode is enabled,
77
+ * `responsiveSteps` are ignored.
78
+ *
79
+ * By default, each field is placed on a new row. To organize fields into rows, there are
80
+ * two options:
81
+ *
82
+ * 1. Use `<vaadin-form-row>` to explicitly group fields into rows.
83
+ *
84
+ * 2. Enable the `autoRows` property to automatically arrange fields in available columns,
85
+ * wrapping to a new row when necessary. `<br>` elements can be used to force a new row.
86
+ *
87
+ * The auto-responsive mode is disabled by default. To enable it for an individual instance,
88
+ * use this property. Alternatively, if you want it to be enabled for all instances by default,
89
+ * enable the `defaultAutoResponsiveFormLayout` feature flag before `<vaadin-form-layout>`
90
+ * elements are added to the DOM:
91
+ *
92
+ * ```js
93
+ * window.Vaadin.featureFlags.defaultAutoResponsiveFormLayout = true;
94
+ * ```
95
+ *
96
+ * @attr {boolean} auto-responsive
97
+ */
98
+ autoResponsive: boolean;
99
+
100
+ /**
101
+ * When `autoResponsive` is enabled, defines the width of each column.
102
+ * The value must be defined in CSS length units, e.g. `100px`.
103
+ *
104
+ * If the column width isn't explicitly set, it defaults to `12em`
105
+ * or `--vaadin-field-default-width` if that CSS property is defined.
106
+ *
107
+ * @attr {string} column-width
108
+ */
109
+ columnWidth: string;
110
+
111
+ /**
112
+ * When `autoResponsive` is enabled, defines the maximum number of columns
113
+ * that the layout can create. The layout will create columns up to this
114
+ * limit based on the available container width.
115
+ *
116
+ * The default value is `10`.
117
+ *
118
+ * @attr {number} max-columns
119
+ */
120
+ maxColumns: number;
121
+
122
+ /**
123
+ * When enabled with `autoResponsive`, distributes fields across columns
124
+ * by placing each field in the next available column and wrapping to
125
+ * the next row when the current row is full. `<br>` elements can be
126
+ * used to force a new row.
127
+ *
128
+ * The default value is `false`.
129
+ *
130
+ * @attr {boolean} auto-rows
131
+ */
132
+ autoRows: boolean;
133
+
134
+ /**
135
+ * When enabled with `autoResponsive`, `<vaadin-form-item>` prefers positioning
136
+ * labels beside the fields. If the layout is too narrow to fit a single column
137
+ * with a side label, the component will automatically switch labels to their
138
+ * default position above the fields.
139
+ *
140
+ * The default value is `false`.
141
+ *
142
+ * To customize the label width and the gap between the label and the field,
143
+ * use the following CSS properties:
144
+ *
145
+ * - `--vaadin-form-layout-label-width`
146
+ * - `--vaadin-form-layout-label-spacing`
147
+ *
148
+ * @attr {boolean} labels-aside
149
+ */
150
+ labelsAside: boolean;
151
+
152
+ /**
153
+ * When `autoResponsive` is enabled, specifies whether the columns should expand
154
+ * in width to evenly fill any remaining space after all columns have been created.
155
+ *
156
+ * The default value is `false`.
157
+ *
158
+ * @attr {boolean} expand-columns
159
+ */
160
+ expandColumns: boolean;
161
+
162
+ /**
163
+ * When `autoResponsive` is enabled, specifies whether fields should stretch
164
+ * to take up all available space within columns. This setting also applies
165
+ * to fields inside `<vaadin-form-item>` elements.
166
+ *
167
+ * The default value is `false`.
168
+ *
169
+ * @attr {boolean} expand-fields
170
+ */
171
+ expandFields: boolean;
172
+
64
173
  /**
65
174
  * Update the layout.
66
175
  */
@@ -3,22 +3,17 @@
3
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
- 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
- }
6
+ import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
7
+ import { AutoResponsiveLayout } from './layouts/auto-responsive-layout.js';
8
+ import { ResponsiveStepsLayout } from './layouts/responsive-steps-layout.js';
9
+ import { formLayoutSlotStyles } from './vaadin-form-layout-styles.js';
15
10
 
16
11
  /**
17
12
  * @polymerMixin
18
- * @mixes ResizeMixin
13
+ * @mixes SlotStylesMixin
19
14
  */
20
15
  export const FormLayoutMixin = (superClass) =>
21
- class extends ResizeMixin(superClass) {
16
+ class extends SlotStylesMixin(superClass) {
22
17
  static get properties() {
23
18
  return {
24
19
  /**
@@ -37,6 +32,14 @@ export const FormLayoutMixin = (superClass) =>
37
32
  * with `minWidth` CSS length, `columns` number, and optional
38
33
  * `labelsPosition` string of `"aside"` or `"top"`. At least one item is required.
39
34
  *
35
+ * NOTE: Responsive steps are ignored in auto-responsive mode, which may be
36
+ * enabled explicitly via the `autoResponsive` property or implicitly
37
+ * if the following feature flag is set:
38
+ *
39
+ * ```
40
+ * window.Vaadin.featureFlags.defaultAutoResponsiveFormLayout = true
41
+ * ```
42
+ *
40
43
  * #### Examples
41
44
  *
42
45
  * ```javascript
@@ -77,100 +80,198 @@ export const FormLayoutMixin = (superClass) =>
77
80
  { minWidth: '40em', columns: 2 },
78
81
  ];
79
82
  },
80
- observer: '_responsiveStepsChanged',
83
+ observer: '__responsiveStepsChanged',
81
84
  sync: true,
82
85
  },
83
86
 
84
87
  /**
85
- * Current number of columns in the layout
86
- * @private
88
+ * When set to `true`, the component automatically creates and adjusts columns based on
89
+ * the container's width. Columns have a fixed width defined by `columnWidth` and their
90
+ * number increases up to the limit set by `maxColumns`. The component dynamically adjusts
91
+ * the number of columns as the container size changes. When this mode is enabled,
92
+ * `responsiveSteps` are ignored.
93
+ *
94
+ * By default, each field is placed on a new row. To organize fields into rows, there are
95
+ * two options:
96
+ *
97
+ * 1. Use `<vaadin-form-row>` to explicitly group fields into rows.
98
+ *
99
+ * 2. Enable the `autoRows` property to automatically arrange fields in available columns,
100
+ * wrapping to a new row when necessary. `<br>` elements can be used to force a new row.
101
+ *
102
+ * The auto-responsive mode is disabled by default. To enable it for an individual instance,
103
+ * use this property. Alternatively, if you want it to be enabled for all instances by default,
104
+ * enable the `defaultAutoResponsiveFormLayout` feature flag before `<vaadin-form-layout>`
105
+ * elements are added to the DOM:
106
+ *
107
+ * ```js
108
+ * window.Vaadin.featureFlags.defaultAutoResponsiveFormLayout = true;
109
+ * ```
110
+ *
111
+ * @attr {boolean} auto-responsive
87
112
  */
88
- _columnCount: {
113
+ autoResponsive: {
114
+ type: Boolean,
115
+ sync: true,
116
+ value: () => {
117
+ if (
118
+ window.Vaadin &&
119
+ window.Vaadin.featureFlags &&
120
+ window.Vaadin.featureFlags.defaultAutoResponsiveFormLayout
121
+ ) {
122
+ return true;
123
+ }
124
+
125
+ return false;
126
+ },
127
+ reflectToAttribute: true,
128
+ },
129
+
130
+ /**
131
+ * When `autoResponsive` is enabled, defines the width of each column.
132
+ * The value must be defined in CSS length units, e.g. `100px`.
133
+ *
134
+ * If the column width isn't explicitly set, it defaults to `12em`
135
+ * or `--vaadin-field-default-width` if that CSS property is defined.
136
+ *
137
+ * @attr {string} column-width
138
+ */
139
+ columnWidth: {
140
+ type: String,
141
+ sync: true,
142
+ },
143
+
144
+ /**
145
+ * When `autoResponsive` is enabled, defines the maximum number of columns
146
+ * that the layout can create. The layout will create columns up to this
147
+ * limit based on the available container width.
148
+ *
149
+ * The default value is `10`.
150
+ *
151
+ * @attr {number} max-columns
152
+ */
153
+ maxColumns: {
89
154
  type: Number,
90
155
  sync: true,
156
+ value: 10,
157
+ },
158
+
159
+ /**
160
+ * When enabled with `autoResponsive`, distributes fields across columns
161
+ * by placing each field in the next available column and wrapping to
162
+ * the next row when the current row is full. `<br>` elements can be
163
+ * used to force a new row.
164
+ *
165
+ * The default value is `false`.
166
+ *
167
+ * @attr {boolean} auto-rows
168
+ */
169
+ autoRows: {
170
+ type: Boolean,
171
+ sync: true,
172
+ value: false,
173
+ reflectToAttribute: true,
174
+ },
175
+
176
+ /**
177
+ * When enabled with `autoResponsive`, `<vaadin-form-item>` prefers positioning
178
+ * labels beside the fields. If the layout is too narrow to fit a single column
179
+ * with a side label, the component will automatically switch labels to their
180
+ * default position above the fields.
181
+ *
182
+ * The default value is `false`.
183
+ *
184
+ * To customize the label width and the gap between the label and the field,
185
+ * use the following CSS properties:
186
+ *
187
+ * - `--vaadin-form-layout-label-width`
188
+ * - `--vaadin-form-layout-label-spacing`
189
+ *
190
+ * @attr {boolean} labels-aside
191
+ */
192
+ labelsAside: {
193
+ type: Boolean,
194
+ sync: true,
195
+ value: false,
196
+ reflectToAttribute: true,
197
+ },
198
+
199
+ /**
200
+ * When `autoResponsive` is enabled, specifies whether the columns should expand
201
+ * in width to evenly fill any remaining space after all columns have been created.
202
+ *
203
+ * The default value is `false`.
204
+ *
205
+ * @attr {boolean} expand-columns
206
+ */
207
+ expandColumns: {
208
+ type: Boolean,
209
+ sync: true,
210
+ value: false,
211
+ reflectToAttribute: true,
91
212
  },
92
213
 
93
214
  /**
94
- * Indicates that labels are on top
95
- * @private
215
+ * When `autoResponsive` is enabled, specifies whether fields should stretch
216
+ * to take up all available space within columns. This setting also applies
217
+ * to fields inside `<vaadin-form-item>` elements.
218
+ *
219
+ * The default value is `false`.
220
+ *
221
+ * @attr {boolean} expand-fields
96
222
  */
97
- _labelsOnTop: {
223
+ expandFields: {
98
224
  type: Boolean,
99
225
  sync: true,
226
+ value: false,
227
+ reflectToAttribute: true,
100
228
  },
101
229
  };
102
230
  }
103
231
 
104
232
  static get observers() {
105
- return ['_invokeUpdateLayout(_columnCount, _labelsOnTop)'];
233
+ return [
234
+ '__autoResponsiveLayoutPropsChanged(columnWidth, maxColumns, autoRows, labelsAside, expandColumns, expandFields)',
235
+ '__autoResponsiveChanged(autoResponsive)',
236
+ ];
106
237
  }
107
238
 
108
- /** @protected */
109
- connectedCallback() {
110
- super.connectedCallback();
239
+ constructor() {
240
+ super();
111
241
 
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 });
242
+ /** @type {import('./layouts/abstract-layout.js').AbstractLayout} */
243
+ this.__currentLayout;
115
244
 
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
- });
245
+ this.__autoResponsiveLayout = new AutoResponsiveLayout(this);
246
+ this.__responsiveStepsLayout = new ResponsiveStepsLayout(this);
247
+ }
127
248
 
128
- requestAnimationFrame(() => this._selectResponsiveStep());
129
- requestAnimationFrame(() => this._updateLayout());
249
+ /** @protected */
250
+ connectedCallback() {
251
+ super.connectedCallback();
252
+ this.__currentLayout.connect();
130
253
  }
131
254
 
132
255
  /** @protected */
133
256
  disconnectedCallback() {
134
257
  super.disconnectedCallback();
258
+ this.__currentLayout.disconnect();
259
+ }
135
260
 
136
- this.__childrenObserver.disconnect();
137
- this.__childrenAttributesObserver.disconnect();
261
+ /** @override */
262
+ get slotStyles() {
263
+ return [`${formLayoutSlotStyles}`.replace('vaadin-form-layout', this.localName)];
138
264
  }
139
265
 
140
- /** @private */
141
- _naturalNumberOrOne(n) {
142
- if (typeof n === 'number' && n >= 1 && n < Infinity) {
143
- return Math.floor(n);
144
- }
145
- return 1;
266
+ /** @protected */
267
+ _updateLayout() {
268
+ this.__currentLayout.updateLayout();
146
269
  }
147
270
 
148
271
  /** @private */
149
- _responsiveStepsChanged(responsiveSteps, oldResponsiveSteps) {
272
+ __responsiveStepsChanged(responsiveSteps, oldResponsiveSteps) {
150
273
  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
- });
274
+ this.__responsiveStepsLayout.setProps({ responsiveSteps });
174
275
  } catch (e) {
175
276
  if (oldResponsiveSteps && oldResponsiveSteps !== responsiveSteps) {
176
277
  console.warn(`${e.message} Using previously set 'responsiveSteps' instead.`);
@@ -184,147 +285,33 @@ export const FormLayoutMixin = (superClass) =>
184
285
  ];
185
286
  }
186
287
  }
187
-
188
- this._selectResponsiveStep();
189
288
  }
190
289
 
191
290
  /** @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
- }
291
+ // eslint-disable-next-line @typescript-eslint/max-params
292
+ __autoResponsiveLayoutPropsChanged(columnWidth, maxColumns, autoRows, labelsAside, expandColumns, expandFields) {
293
+ this.__autoResponsiveLayout.setProps({
294
+ columnWidth,
295
+ maxColumns,
296
+ autoRows,
297
+ labelsAside,
298
+ expandColumns,
299
+ expandFields,
205
300
  });
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
301
  }
216
302
 
217
303
  /** @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;
304
+ __autoResponsiveChanged(autoResponsive) {
305
+ if (this.__currentLayout) {
306
+ this.__currentLayout.disconnect();
230
307
  }
231
308
 
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;
309
+ if (autoResponsive) {
310
+ this.__currentLayout = this.__autoResponsiveLayout;
311
+ } else {
312
+ this.__currentLayout = this.__responsiveStepsLayout;
323
313
  }
324
314
 
325
- this._selectResponsiveStep();
326
- this._updateLayout();
327
-
328
- this.$.layout.style.opacity = '';
315
+ this.__currentLayout.connect();
329
316
  }
330
317
  };