@vaadin/form-layout 24.7.0-rc1 → 24.8.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": "24.7.0-rc1",
3
+ "version": "24.8.0-alpha2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -37,24 +37,24 @@
37
37
  "dependencies": {
38
38
  "@open-wc/dedupe-mixin": "^1.3.0",
39
39
  "@polymer/polymer": "^3.0.0",
40
- "@vaadin/a11y-base": "24.7.0-rc1",
41
- "@vaadin/component-base": "24.7.0-rc1",
42
- "@vaadin/vaadin-lumo-styles": "24.7.0-rc1",
43
- "@vaadin/vaadin-material-styles": "24.7.0-rc1",
44
- "@vaadin/vaadin-themable-mixin": "24.7.0-rc1",
40
+ "@vaadin/a11y-base": "24.8.0-alpha2",
41
+ "@vaadin/component-base": "24.8.0-alpha2",
42
+ "@vaadin/vaadin-lumo-styles": "24.8.0-alpha2",
43
+ "@vaadin/vaadin-material-styles": "24.8.0-alpha2",
44
+ "@vaadin/vaadin-themable-mixin": "24.8.0-alpha2",
45
45
  "lit": "^3.0.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@vaadin/chai-plugins": "24.7.0-rc1",
49
- "@vaadin/custom-field": "24.7.0-rc1",
50
- "@vaadin/test-runner-commands": "24.7.0-rc1",
48
+ "@vaadin/chai-plugins": "24.8.0-alpha2",
49
+ "@vaadin/custom-field": "24.8.0-alpha2",
50
+ "@vaadin/test-runner-commands": "24.8.0-alpha2",
51
51
  "@vaadin/testing-helpers": "^1.1.0",
52
- "@vaadin/text-field": "24.7.0-rc1",
52
+ "@vaadin/text-field": "24.8.0-alpha2",
53
53
  "sinon": "^18.0.0"
54
54
  },
55
55
  "web-types": [
56
56
  "web-types.json",
57
57
  "web-types.lit.json"
58
58
  ],
59
- "gitHead": "28f6d361ebf39070c0961cee75ee6c14089109b2"
59
+ "gitHead": "f48777a6e3dcf605b700305a7142145e197bb416"
60
60
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2025 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+
7
+ /**
8
+ * An abstract class for layout implementation. Not intended for public use.
9
+ *
10
+ * @private
11
+ */
12
+ export class AbstractLayout {
13
+ /**
14
+ * @param {HTMLElement} host
15
+ * @param {{ mutationObserverOptions: MutationObserverInit }} config
16
+ */
17
+ constructor(host, config) {
18
+ this.host = host;
19
+ this.props = {};
20
+ this.config = config;
21
+ this.isConnected = false;
22
+
23
+ /** @private */
24
+ this.__resizeObserver = new ResizeObserver((entries) => setTimeout(() => this._onResize(entries)));
25
+
26
+ /** @private */
27
+ this.__mutationObserver = new MutationObserver((records) => this._onMutation(records));
28
+ }
29
+
30
+ /**
31
+ * Connects the layout to the host element.
32
+ */
33
+ connect() {
34
+ if (this.isConnected) {
35
+ return;
36
+ }
37
+
38
+ this.isConnected = true;
39
+ this.__resizeObserver.observe(this.host);
40
+ this.__mutationObserver.observe(this.host, this.config.mutationObserverOptions);
41
+ }
42
+
43
+ /**
44
+ * Disconnects the layout from the host element.
45
+ */
46
+ disconnect() {
47
+ if (!this.isConnected) {
48
+ return;
49
+ }
50
+
51
+ this.isConnected = false;
52
+ this.__resizeObserver.disconnect();
53
+ this.__mutationObserver.disconnect();
54
+ }
55
+
56
+ /**
57
+ * Sets the properties of the layout controller.
58
+ */
59
+ setProps(props) {
60
+ this.props = props;
61
+ }
62
+
63
+ /**
64
+ * Updates the layout based on the current properties.
65
+ */
66
+ updateLayout() {
67
+ // To be implemented
68
+ }
69
+
70
+ /**
71
+ * @param {ResizeObserverEntry[]} _entries
72
+ * @protected
73
+ */
74
+ _onResize(_entries) {
75
+ // To be implemented
76
+ }
77
+
78
+ /**
79
+ * @param {MutationRecord[]} _records
80
+ * @protected
81
+ */
82
+ _onMutation(_records) {
83
+ // To be implemented
84
+ }
85
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 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';
7
+ import { AbstractLayout } from './abstract-layout.js';
8
+
9
+ /**
10
+ * Check if the node is a line break element.
11
+ *
12
+ * @param {HTMLElement} el
13
+ * @return {boolean}
14
+ */
15
+ function isBreakLine(el) {
16
+ return el.localName === 'br';
17
+ }
18
+
19
+ /**
20
+ * A class that implements the auto-responsive layout algorithm.
21
+ * Not intended for public use.
22
+ *
23
+ * @private
24
+ */
25
+ export class AutoResponsiveLayout extends AbstractLayout {
26
+ constructor(host) {
27
+ super(host, {
28
+ mutationObserverOptions: {
29
+ subtree: true,
30
+ childList: true,
31
+ attributes: true,
32
+ attributeFilter: ['colspan', 'data-colspan', 'hidden'],
33
+ },
34
+ });
35
+ }
36
+
37
+ /** @override */
38
+ connect() {
39
+ if (this.isConnected) {
40
+ return;
41
+ }
42
+
43
+ super.connect();
44
+
45
+ this.updateLayout();
46
+ }
47
+
48
+ /** @override */
49
+ disconnect() {
50
+ if (!this.isConnected) {
51
+ return;
52
+ }
53
+
54
+ super.disconnect();
55
+
56
+ const { host } = this;
57
+ host.style.removeProperty('--_column-width');
58
+ host.style.removeProperty('--_max-columns');
59
+ host.$.layout.removeAttribute('fits-labels-aside');
60
+ host.$.layout.style.removeProperty('--_grid-rendered-column-count');
61
+
62
+ this.__children.forEach((child) => {
63
+ child.style.removeProperty('--_grid-colstart');
64
+ child.style.removeProperty('--_grid-colspan');
65
+ });
66
+ }
67
+
68
+ /** @override */
69
+ setProps(props) {
70
+ super.setProps(props);
71
+
72
+ if (this.isConnected) {
73
+ this.updateLayout();
74
+ }
75
+ }
76
+
77
+ /** @override */
78
+ updateLayout() {
79
+ const { host, props } = this;
80
+ if (!this.isConnected || isElementHidden(host)) {
81
+ return;
82
+ }
83
+
84
+ let columnCount = 0;
85
+ let maxColumns = 0;
86
+
87
+ const children = this.__children;
88
+ children
89
+ .filter((child) => isBreakLine(child) || !isElementHidden(child))
90
+ .forEach((child, index, children) => {
91
+ const prevChild = children[index - 1];
92
+
93
+ if (isBreakLine(child)) {
94
+ columnCount = 0;
95
+ return;
96
+ }
97
+
98
+ if (
99
+ (prevChild && prevChild.parentElement !== child.parentElement) ||
100
+ (!props.autoRows && child.parentElement === host)
101
+ ) {
102
+ columnCount = 0;
103
+ }
104
+
105
+ if (props.autoRows && columnCount === 0) {
106
+ child.style.setProperty('--_grid-colstart', 1);
107
+ } else {
108
+ child.style.removeProperty('--_grid-colstart');
109
+ }
110
+
111
+ const colspan = child.getAttribute('colspan') || child.getAttribute('data-colspan');
112
+ if (colspan) {
113
+ columnCount += parseInt(colspan);
114
+ child.style.setProperty('--_grid-colspan', colspan);
115
+ } else {
116
+ columnCount += 1;
117
+ child.style.removeProperty('--_grid-colspan');
118
+ }
119
+
120
+ maxColumns = Math.max(maxColumns, columnCount);
121
+ });
122
+
123
+ children.filter(isElementHidden).forEach((child) => {
124
+ child.style.removeProperty('--_grid-colstart');
125
+ });
126
+
127
+ host.style.setProperty('--_column-width', props.columnWidth);
128
+ host.style.setProperty('--_max-columns', Math.min(props.maxColumns, maxColumns));
129
+
130
+ host.$.layout.toggleAttribute('fits-labels-aside', this.props.labelsAside && this.__fitsLabelsAside);
131
+ host.$.layout.style.setProperty('--_grid-rendered-column-count', this.__renderedColumnCount);
132
+ }
133
+
134
+ /** @override */
135
+ _onResize() {
136
+ this.updateLayout();
137
+ }
138
+
139
+ /** @override */
140
+ _onMutation(records) {
141
+ const shouldUpdateLayout = records.some(({ target }) => {
142
+ return (
143
+ target === this.host ||
144
+ target.parentElement === this.host ||
145
+ target.parentElement.localName === 'vaadin-form-row'
146
+ );
147
+ });
148
+ if (shouldUpdateLayout) {
149
+ this.updateLayout();
150
+ }
151
+ }
152
+
153
+ /** @private */
154
+ get __children() {
155
+ return [...this.host.children].flatMap((child) => {
156
+ return child.localName === 'vaadin-form-row' ? [...child.children] : child;
157
+ });
158
+ }
159
+
160
+ /** @private */
161
+ get __renderedColumnCount() {
162
+ // Calculate the number of rendered columns, excluding CSS grid auto columns (0px)
163
+ const { gridTemplateColumns } = getComputedStyle(this.host.$.layout);
164
+ return gridTemplateColumns.split(' ').filter((width) => width !== '0px').length;
165
+ }
166
+
167
+ /** @private */
168
+ get __columnWidthWithLabelsAside() {
169
+ const { backgroundPositionY } = getComputedStyle(this.host.$.layout, '::before');
170
+ return parseFloat(backgroundPositionY);
171
+ }
172
+
173
+ /** @private */
174
+ get __fitsLabelsAside() {
175
+ return this.host.offsetWidth >= this.__columnWidthWithLabelsAside;
176
+ }
177
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 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';
7
+ import { AbstractLayout } from './abstract-layout.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
+ function naturalNumberOrOne(n) {
17
+ if (typeof n === 'number' && n >= 1 && n < Infinity) {
18
+ return Math.floor(n);
19
+ }
20
+ return 1;
21
+ }
22
+
23
+ /**
24
+ * A class that implements the layout algorithm based on responsive steps.
25
+ * Not intended for public use.
26
+ *
27
+ * @private
28
+ */
29
+ export class ResponsiveStepsLayout extends AbstractLayout {
30
+ constructor(host) {
31
+ super(host, {
32
+ mutationObserverOptions: {
33
+ subtree: true,
34
+ childList: true,
35
+ attributes: true,
36
+ attributeFilter: ['colspan', 'data-colspan', 'hidden'],
37
+ },
38
+ });
39
+ }
40
+
41
+ /** @override */
42
+ connect() {
43
+ if (this.isConnected) {
44
+ return;
45
+ }
46
+
47
+ super.connect();
48
+
49
+ this.__selectResponsiveStep();
50
+ this.updateLayout();
51
+
52
+ requestAnimationFrame(() => this.__selectResponsiveStep());
53
+ requestAnimationFrame(() => this.updateLayout());
54
+ }
55
+
56
+ /** @override */
57
+ setProps(props) {
58
+ const { responsiveSteps } = props;
59
+ if (!Array.isArray(responsiveSteps)) {
60
+ throw new Error('Invalid "responsiveSteps" type, an Array is required.');
61
+ }
62
+
63
+ if (responsiveSteps.length < 1) {
64
+ throw new Error('Invalid empty "responsiveSteps" array, at least one item is required.');
65
+ }
66
+
67
+ responsiveSteps.forEach((step) => {
68
+ if (naturalNumberOrOne(step.columns) !== step.columns) {
69
+ throw new Error(`Invalid 'columns' value of ${step.columns}, a natural number is required.`);
70
+ }
71
+
72
+ if (step.minWidth !== undefined && !isValidCSSLength(step.minWidth)) {
73
+ throw new Error(`Invalid 'minWidth' value of ${step.minWidth}, a valid CSS length required.`);
74
+ }
75
+
76
+ if (step.labelsPosition !== undefined && ['aside', 'top'].indexOf(step.labelsPosition) === -1) {
77
+ throw new Error(
78
+ `Invalid 'labelsPosition' value of ${step.labelsPosition}, 'aside' or 'top' string is required.`,
79
+ );
80
+ }
81
+ });
82
+
83
+ super.setProps(props);
84
+
85
+ if (this.isConnected) {
86
+ this.__selectResponsiveStep();
87
+ this.updateLayout();
88
+ }
89
+ }
90
+
91
+ /** @override */
92
+ updateLayout() {
93
+ const { host } = this;
94
+
95
+ // Do not update layout when invisible
96
+ if (!this.isConnected || isElementHidden(host)) {
97
+ return;
98
+ }
99
+
100
+ /*
101
+ The item width formula:
102
+
103
+ itemWidth = colspan / columnCount * 100% - columnSpacing
104
+
105
+ We have to subtract columnSpacing, because the column spacing space is taken
106
+ by item margins of 1/2 * spacing on both sides
107
+ */
108
+
109
+ const style = getComputedStyle(host);
110
+ const columnSpacing = style.getPropertyValue('--vaadin-form-layout-column-spacing');
111
+
112
+ const direction = style.direction;
113
+ const marginStartProp = `margin-${direction === 'ltr' ? 'left' : 'right'}`;
114
+ const marginEndProp = `margin-${direction === 'ltr' ? 'right' : 'left'}`;
115
+
116
+ const containerWidth = host.offsetWidth;
117
+
118
+ let col = 0;
119
+ Array.from(host.children)
120
+ .filter((child) => child.localName === 'br' || getComputedStyle(child).display !== 'none')
121
+ .forEach((child, index, children) => {
122
+ if (child.localName === 'br') {
123
+ // Reset column count on line break
124
+ col = 0;
125
+ return;
126
+ }
127
+
128
+ const attrColspan = child.getAttribute('colspan') || child.getAttribute('data-colspan');
129
+ let colspan;
130
+ colspan = naturalNumberOrOne(parseFloat(attrColspan));
131
+
132
+ // Never span further than the number of columns
133
+ colspan = Math.min(colspan, this.__columnCount);
134
+
135
+ const childRatio = colspan / this.__columnCount;
136
+ child.style.width = `calc(${childRatio * 100}% - ${1 - childRatio} * ${columnSpacing})`;
137
+
138
+ if (col + colspan > this.__columnCount) {
139
+ // Too big to fit on this row, let's wrap it
140
+ col = 0;
141
+ }
142
+
143
+ // At the start edge
144
+ if (col === 0) {
145
+ child.style.setProperty(marginStartProp, '0px');
146
+ } else {
147
+ child.style.removeProperty(marginStartProp);
148
+ }
149
+
150
+ const nextIndex = index + 1;
151
+ const nextLineBreak = nextIndex < children.length && children[nextIndex].localName === 'br';
152
+
153
+ // At the end edge
154
+ if (col + colspan === this.__columnCount) {
155
+ child.style.setProperty(marginEndProp, '0px');
156
+ } else if (nextLineBreak) {
157
+ const colspanRatio = (this.__columnCount - col - colspan) / this.__columnCount;
158
+ child.style.setProperty(
159
+ marginEndProp,
160
+ `calc(${colspanRatio * containerWidth}px + ${colspanRatio} * ${columnSpacing})`,
161
+ );
162
+ } else {
163
+ child.style.removeProperty(marginEndProp);
164
+ }
165
+
166
+ // Move the column counter
167
+ col = (col + colspan) % this.__columnCount;
168
+
169
+ if (child.localName === 'vaadin-form-item') {
170
+ if (this.__labelsOnTop) {
171
+ if (child.getAttribute('label-position') !== 'top') {
172
+ child.__useLayoutLabelPosition = true;
173
+ child.setAttribute('label-position', 'top');
174
+ }
175
+ } else if (child.__useLayoutLabelPosition) {
176
+ delete child.__useLayoutLabelPosition;
177
+ child.removeAttribute('label-position');
178
+ }
179
+ }
180
+ });
181
+ }
182
+
183
+ /** @override */
184
+ _onResize() {
185
+ const { host } = this;
186
+ if (isElementHidden(host)) {
187
+ host.$.layout.style.opacity = '0';
188
+ return;
189
+ }
190
+
191
+ this.__selectResponsiveStep();
192
+ this.updateLayout();
193
+
194
+ host.$.layout.style.opacity = '';
195
+ }
196
+
197
+ /** @override */
198
+ _onMutation(records) {
199
+ const shouldUpdateLayout = records.some(({ target }) => {
200
+ return target === this.host || target.parentElement === this.host;
201
+ });
202
+ if (shouldUpdateLayout) {
203
+ this.updateLayout();
204
+ }
205
+ }
206
+
207
+ /** @private */
208
+ __selectResponsiveStep() {
209
+ const { host, props } = this;
210
+ // Iterate through responsiveSteps and choose the step
211
+ let selectedStep;
212
+ const tmpStyleProp = 'background-position';
213
+ props.responsiveSteps.forEach((step) => {
214
+ // Convert minWidth to px units for comparison
215
+ host.$.layout.style.setProperty(tmpStyleProp, step.minWidth);
216
+ const stepMinWidthPx = parseFloat(getComputedStyle(host.$.layout).getPropertyValue(tmpStyleProp));
217
+
218
+ // Compare step min-width with the host width, select the passed step
219
+ if (stepMinWidthPx <= host.offsetWidth) {
220
+ selectedStep = step;
221
+ }
222
+ });
223
+ host.$.layout.style.removeProperty(tmpStyleProp);
224
+
225
+ // Sometimes converting units is not possible, e.g, when element is
226
+ // not connected. Then the `selectedStep` stays `undefined`.
227
+ if (selectedStep) {
228
+ // Apply the chosen responsive step's properties
229
+ this.__columnCount = selectedStep.columns;
230
+ this.__labelsOnTop = selectedStep.labelsPosition === 'top';
231
+ }
232
+ }
233
+ }
@@ -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
  /**
@@ -61,6 +61,87 @@ export declare class FormLayoutMixinClass {
61
61
  */
62
62
  responsiveSteps: FormLayoutResponsiveStep[];
63
63
 
64
+ /**
65
+ * Enables the auto responsive mode in which the component automatically creates and adjusts
66
+ * columns based on the container's width. Columns have a fixed width defined by `columnWidth`
67
+ * and their number increases up to the limit set by `maxColumns`. The component dynamically
68
+ * adjusts the number of columns as the container size changes. When this mode is enabled,
69
+ * the `responsiveSteps` are ignored.
70
+ *
71
+ * By default, each field is placed on a new row. To organize fields into rows, there are
72
+ * two options:
73
+ *
74
+ * 1. Use `<vaadin-form-row>` to explicitly group fields into rows.
75
+ *
76
+ * 2. Enable the `autoRows` property to automatically arrange fields in available columns,
77
+ * wrapping to a new row when necessary. `<br>` elements can be used to force a new row.
78
+ *
79
+ * @attr {boolean} auto-responsive
80
+ */
81
+ autoResponsive: boolean;
82
+
83
+ /**
84
+ * When `autoResponsive` is enabled, defines the width of each column.
85
+ * The value must be defined in CSS length units, e.g., `100px` or `13em`.
86
+ * The default value is `13em`.
87
+ *
88
+ * @attr {string} column-width
89
+ */
90
+ columnWidth: string;
91
+
92
+ /**
93
+ * When `autoResponsive` is enabled, defines the maximum number of columns
94
+ * that the layout can create. The layout will create columns up to this
95
+ * limit based on the available container width. The default value is `10`.
96
+ *
97
+ * @attr {number} max-columns
98
+ */
99
+ maxColumns: number;
100
+
101
+ /**
102
+ * When enabled with `autoResponsive`, distributes fields across columns
103
+ * by placing each field in the next available column and wrapping to
104
+ * the next row when the current row is full. `<br>` elements can be
105
+ * used to force a new row.
106
+ *
107
+ * @attr {boolean} auto-rows
108
+ */
109
+ autoRows: boolean;
110
+
111
+ /**
112
+ * When enabled with `autoResponsive`, `<vaadin-form-item>` prefers positioning
113
+ * labels beside the fields. If the layout is too narrow to fit a single column
114
+ * with side labels, they revert to their default position above the fields.
115
+ *
116
+ * To customize the label width and the gap between the label and the field,
117
+ * use the following CSS properties:
118
+ *
119
+ * - `--vaadin-form-layout-label-width`
120
+ * - `--vaadin-form-layout-label-spacing`
121
+ *
122
+ * @attr {boolean} labels-aside
123
+ */
124
+ labelsAside: boolean;
125
+
126
+ /**
127
+ * When `autoResponsive` is enabled, specifies whether the columns should expand
128
+ * in width to evenly fill any remaining space after the layout has created as
129
+ * many fixed-width (`columnWidth`) columns as possible within the `maxColumns`
130
+ * limit. The default value is `false`.
131
+ *
132
+ * @attr {boolean} expand-columns
133
+ */
134
+ expandColumns: boolean;
135
+
136
+ /**
137
+ * When `autoResponsive` is enabled, specifies whether fields should stretch
138
+ * to take up all available space within columns. This setting also applies
139
+ * to fields inside `<vaadin-form-item>` elements. The default value is `false`.
140
+ *
141
+ * @attr {boolean} expand-fields
142
+ */
143
+ expandFields: boolean;
144
+
64
145
  /**
65
146
  * Update the layout.
66
147
  */