@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.
- package/package.json +11 -11
- package/src/layouts/abstract-layout.js +85 -0
- package/src/layouts/auto-responsive-layout.js +182 -0
- package/src/layouts/responsive-steps-layout.js +255 -0
- package/src/vaadin-form-item-mixin.js +33 -27
- package/src/vaadin-form-layout-mixin.d.ts +111 -2
- package/src/vaadin-form-layout-mixin.js +188 -201
- package/src/vaadin-form-layout-styles.js +159 -11
- package/src/vaadin-form-layout.d.ts +97 -0
- package/src/vaadin-form-layout.js +95 -0
- package/src/vaadin-form-row.d.ts +24 -0
- package/src/vaadin-form-row.js +37 -0
- package/src/vaadin-lit-form-row.d.ts +6 -0
- package/src/vaadin-lit-form-row.js +38 -0
- package/theme/lumo/vaadin-form-row.d.ts +1 -0
- package/theme/lumo/vaadin-form-row.js +1 -0
- package/theme/lumo/vaadin-lit-form-row.d.ts +1 -0
- package/theme/lumo/vaadin-lit-form-row.js +1 -0
- package/theme/material/vaadin-form-row.d.ts +1 -0
- package/theme/material/vaadin-form-row.js +1 -0
- package/theme/material/vaadin-lit-form-row.d.ts +1 -0
- package/theme/material/vaadin-lit-form-row.js +1 -0
- package/vaadin-form-row.d.ts +1 -0
- package/vaadin-form-row.js +2 -0
- package/vaadin-lit-form-row.d.ts +1 -0
- package/vaadin-lit-form-row.js +2 -0
- package/web-types.json +178 -3
- package/web-types.lit.json +58 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/form-layout",
|
|
3
|
-
"version": "24.
|
|
3
|
+
"version": "24.8.0-alpha10",
|
|
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": "
|
|
41
|
-
"@vaadin/component-base": "
|
|
42
|
-
"@vaadin/vaadin-lumo-styles": "
|
|
43
|
-
"@vaadin/vaadin-material-styles": "
|
|
44
|
-
"@vaadin/vaadin-themable-mixin": "
|
|
40
|
+
"@vaadin/a11y-base": "24.8.0-alpha10",
|
|
41
|
+
"@vaadin/component-base": "24.8.0-alpha10",
|
|
42
|
+
"@vaadin/vaadin-lumo-styles": "24.8.0-alpha10",
|
|
43
|
+
"@vaadin/vaadin-material-styles": "24.8.0-alpha10",
|
|
44
|
+
"@vaadin/vaadin-themable-mixin": "24.8.0-alpha10",
|
|
45
45
|
"lit": "^3.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@vaadin/chai-plugins": "
|
|
49
|
-
"@vaadin/custom-field": "
|
|
50
|
-
"@vaadin/test-runner-commands": "
|
|
48
|
+
"@vaadin/chai-plugins": "24.8.0-alpha10",
|
|
49
|
+
"@vaadin/custom-field": "24.8.0-alpha10",
|
|
50
|
+
"@vaadin/test-runner-commands": "24.8.0-alpha10",
|
|
51
51
|
"@vaadin/testing-helpers": "^1.1.0",
|
|
52
|
-
"@vaadin/text-field": "
|
|
52
|
+
"@vaadin/text-field": "24.8.0-alpha10",
|
|
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": "
|
|
59
|
+
"gitHead": "f8c79ffc67eccc3ade226dfe52fbf7d3d46428cf"
|
|
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,182 @@
|
|
|
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.js';
|
|
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
|
+
if (props.columnWidth) {
|
|
128
|
+
host.style.setProperty('--_column-width', props.columnWidth);
|
|
129
|
+
} else {
|
|
130
|
+
host.style.removeProperty('--_column-width');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
host.style.setProperty('--_max-columns', Math.min(props.maxColumns, maxColumns));
|
|
134
|
+
|
|
135
|
+
host.$.layout.toggleAttribute('fits-labels-aside', this.props.labelsAside && this.__fitsLabelsAside);
|
|
136
|
+
host.$.layout.style.setProperty('--_grid-rendered-column-count', this.__renderedColumnCount);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @override */
|
|
140
|
+
_onResize() {
|
|
141
|
+
this.updateLayout();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @override */
|
|
145
|
+
_onMutation(records) {
|
|
146
|
+
const shouldUpdateLayout = records.some(({ target }) => {
|
|
147
|
+
return (
|
|
148
|
+
target === this.host ||
|
|
149
|
+
target.parentElement === this.host ||
|
|
150
|
+
target.parentElement.localName === 'vaadin-form-row'
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
if (shouldUpdateLayout) {
|
|
154
|
+
this.updateLayout();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** @private */
|
|
159
|
+
get __children() {
|
|
160
|
+
return [...this.host.children].flatMap((child) => {
|
|
161
|
+
return child.localName === 'vaadin-form-row' ? [...child.children] : child;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** @private */
|
|
166
|
+
get __renderedColumnCount() {
|
|
167
|
+
// Calculate the number of rendered columns, excluding CSS grid auto columns (0px)
|
|
168
|
+
const { gridTemplateColumns } = getComputedStyle(this.host.$.layout);
|
|
169
|
+
return gridTemplateColumns.split(' ').filter((width) => width !== '0px').length;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @private */
|
|
173
|
+
get __columnWidthWithLabelsAside() {
|
|
174
|
+
const { backgroundPositionY } = getComputedStyle(this.host.$.layout, '::before');
|
|
175
|
+
return parseFloat(backgroundPositionY);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** @private */
|
|
179
|
+
get __fitsLabelsAside() {
|
|
180
|
+
return this.host.offsetWidth >= this.__columnWidthWithLabelsAside;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
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.js';
|
|
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
|
+
disconnect() {
|
|
58
|
+
if (!this.isConnected) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
super.disconnect();
|
|
63
|
+
|
|
64
|
+
const { host } = this;
|
|
65
|
+
host.$.layout.style.removeProperty('opacity');
|
|
66
|
+
[...host.children].forEach((child) => {
|
|
67
|
+
child.style.removeProperty('width');
|
|
68
|
+
child.style.removeProperty('margin-left');
|
|
69
|
+
child.style.removeProperty('margin-right');
|
|
70
|
+
child.removeAttribute('label-position');
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @override */
|
|
75
|
+
setProps(props) {
|
|
76
|
+
const { responsiveSteps } = props;
|
|
77
|
+
if (!Array.isArray(responsiveSteps)) {
|
|
78
|
+
throw new Error('Invalid "responsiveSteps" type, an Array is required.');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (responsiveSteps.length < 1) {
|
|
82
|
+
throw new Error('Invalid empty "responsiveSteps" array, at least one item is required.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
responsiveSteps.forEach((step) => {
|
|
86
|
+
if (naturalNumberOrOne(step.columns) !== step.columns) {
|
|
87
|
+
throw new Error(`Invalid 'columns' value of ${step.columns}, a natural number is required.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (step.minWidth !== undefined && !isValidCSSLength(step.minWidth)) {
|
|
91
|
+
throw new Error(`Invalid 'minWidth' value of ${step.minWidth}, a valid CSS length required.`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (step.labelsPosition !== undefined && ['aside', 'top'].indexOf(step.labelsPosition) === -1) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Invalid 'labelsPosition' value of ${step.labelsPosition}, 'aside' or 'top' string is required.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
super.setProps(props);
|
|
102
|
+
|
|
103
|
+
if (this.isConnected) {
|
|
104
|
+
this.__selectResponsiveStep();
|
|
105
|
+
this.updateLayout();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @override */
|
|
110
|
+
updateLayout() {
|
|
111
|
+
const { host } = this;
|
|
112
|
+
|
|
113
|
+
// Do not update layout when invisible
|
|
114
|
+
if (!this.isConnected || isElementHidden(host)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/*
|
|
119
|
+
The item width formula:
|
|
120
|
+
|
|
121
|
+
itemWidth = colspan / columnCount * 100% - columnSpacing
|
|
122
|
+
|
|
123
|
+
We have to subtract columnSpacing, because the column spacing space is taken
|
|
124
|
+
by item margins of 1/2 * spacing on both sides
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
const style = getComputedStyle(host);
|
|
128
|
+
const columnSpacing = style.getPropertyValue('--vaadin-form-layout-column-spacing');
|
|
129
|
+
|
|
130
|
+
const direction = style.direction;
|
|
131
|
+
const marginStartProp = `margin-${direction === 'ltr' ? 'left' : 'right'}`;
|
|
132
|
+
const marginEndProp = `margin-${direction === 'ltr' ? 'right' : 'left'}`;
|
|
133
|
+
|
|
134
|
+
const containerWidth = host.offsetWidth;
|
|
135
|
+
|
|
136
|
+
let col = 0;
|
|
137
|
+
Array.from(host.children)
|
|
138
|
+
.filter((child) => child.localName === 'br' || getComputedStyle(child).display !== 'none')
|
|
139
|
+
.forEach((child, index, children) => {
|
|
140
|
+
if (child.localName === 'br') {
|
|
141
|
+
// Reset column count on line break
|
|
142
|
+
col = 0;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const attrColspan = child.getAttribute('colspan') || child.getAttribute('data-colspan');
|
|
147
|
+
let colspan;
|
|
148
|
+
colspan = naturalNumberOrOne(parseFloat(attrColspan));
|
|
149
|
+
|
|
150
|
+
// Never span further than the number of columns
|
|
151
|
+
colspan = Math.min(colspan, this.__columnCount);
|
|
152
|
+
|
|
153
|
+
const childRatio = colspan / this.__columnCount;
|
|
154
|
+
child.style.width = `calc(${childRatio * 100}% - ${1 - childRatio} * ${columnSpacing})`;
|
|
155
|
+
|
|
156
|
+
if (col + colspan > this.__columnCount) {
|
|
157
|
+
// Too big to fit on this row, let's wrap it
|
|
158
|
+
col = 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// At the start edge
|
|
162
|
+
if (col === 0) {
|
|
163
|
+
child.style.setProperty(marginStartProp, '0px');
|
|
164
|
+
} else {
|
|
165
|
+
child.style.removeProperty(marginStartProp);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const nextIndex = index + 1;
|
|
169
|
+
const nextLineBreak = nextIndex < children.length && children[nextIndex].localName === 'br';
|
|
170
|
+
|
|
171
|
+
// At the end edge
|
|
172
|
+
if (col + colspan === this.__columnCount) {
|
|
173
|
+
child.style.setProperty(marginEndProp, '0px');
|
|
174
|
+
} else if (nextLineBreak) {
|
|
175
|
+
const colspanRatio = (this.__columnCount - col - colspan) / this.__columnCount;
|
|
176
|
+
child.style.setProperty(
|
|
177
|
+
marginEndProp,
|
|
178
|
+
`calc(${colspanRatio * containerWidth}px + ${colspanRatio} * ${columnSpacing})`,
|
|
179
|
+
);
|
|
180
|
+
} else {
|
|
181
|
+
child.style.removeProperty(marginEndProp);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Move the column counter
|
|
185
|
+
col = (col + colspan) % this.__columnCount;
|
|
186
|
+
|
|
187
|
+
if (child.localName === 'vaadin-form-item') {
|
|
188
|
+
if (this.__labelsOnTop) {
|
|
189
|
+
if (child.getAttribute('label-position') !== 'top') {
|
|
190
|
+
child.__useLayoutLabelPosition = true;
|
|
191
|
+
child.setAttribute('label-position', 'top');
|
|
192
|
+
}
|
|
193
|
+
} else if (child.__useLayoutLabelPosition) {
|
|
194
|
+
delete child.__useLayoutLabelPosition;
|
|
195
|
+
child.removeAttribute('label-position');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @override */
|
|
202
|
+
_onResize() {
|
|
203
|
+
const { host } = this;
|
|
204
|
+
if (isElementHidden(host)) {
|
|
205
|
+
host.$.layout.style.opacity = '0';
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.__selectResponsiveStep();
|
|
210
|
+
this.updateLayout();
|
|
211
|
+
|
|
212
|
+
host.$.layout.style.opacity = '';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** @override */
|
|
216
|
+
_onMutation(records) {
|
|
217
|
+
const shouldUpdateLayout = records.some(({ target }) => {
|
|
218
|
+
return target === this.host || target.parentElement === this.host;
|
|
219
|
+
});
|
|
220
|
+
if (shouldUpdateLayout) {
|
|
221
|
+
this.updateLayout();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @private */
|
|
226
|
+
__selectResponsiveStep() {
|
|
227
|
+
if (!this.isConnected) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const { host, props } = this;
|
|
232
|
+
// Iterate through responsiveSteps and choose the step
|
|
233
|
+
let selectedStep;
|
|
234
|
+
const tmpStyleProp = 'background-position';
|
|
235
|
+
props.responsiveSteps.forEach((step) => {
|
|
236
|
+
// Convert minWidth to px units for comparison
|
|
237
|
+
host.$.layout.style.setProperty(tmpStyleProp, step.minWidth);
|
|
238
|
+
const stepMinWidthPx = parseFloat(getComputedStyle(host.$.layout).getPropertyValue(tmpStyleProp));
|
|
239
|
+
|
|
240
|
+
// Compare step min-width with the host width, select the passed step
|
|
241
|
+
if (stepMinWidthPx <= host.offsetWidth) {
|
|
242
|
+
selectedStep = step;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
host.$.layout.style.removeProperty(tmpStyleProp);
|
|
246
|
+
|
|
247
|
+
// Sometimes converting units is not possible, e.g, when element is
|
|
248
|
+
// not connected. Then the `selectedStep` stays `undefined`.
|
|
249
|
+
if (selectedStep) {
|
|
250
|
+
// Apply the chosen responsive step's properties
|
|
251
|
+
this.__columnCount = selectedStep.columns;
|
|
252
|
+
this.__labelsOnTop = selectedStep.labelsPosition === 'top';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -17,7 +17,8 @@ export const FormItemMixin = (superClass) =>
|
|
|
17
17
|
class extends superClass {
|
|
18
18
|
constructor() {
|
|
19
19
|
super();
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
this.__onFieldInteraction = this.__onFieldInteraction.bind(this);
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* An observer for a field node to reflect its `required` and `invalid` attributes to the component.
|
|
@@ -25,7 +26,7 @@ export const FormItemMixin = (superClass) =>
|
|
|
25
26
|
* @type {MutationObserver}
|
|
26
27
|
* @private
|
|
27
28
|
*/
|
|
28
|
-
this.__fieldNodeObserver = new MutationObserver(() => this.
|
|
29
|
+
this.__fieldNodeObserver = new MutationObserver(() => this.__synchronizeAttributes());
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* The first label node in the label slot.
|
|
@@ -44,6 +45,8 @@ export const FormItemMixin = (superClass) =>
|
|
|
44
45
|
* @private
|
|
45
46
|
*/
|
|
46
47
|
this.__fieldNode = null;
|
|
48
|
+
|
|
49
|
+
this.__isFieldDirty = false;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/** @protected */
|
|
@@ -122,11 +125,6 @@ export const FormItemMixin = (superClass) =>
|
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
/** @private */
|
|
126
|
-
__getValidateFunction(field) {
|
|
127
|
-
return field.validate || field.checkValidity;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
128
|
/**
|
|
131
129
|
* A `slotchange` event handler for the label slot.
|
|
132
130
|
*
|
|
@@ -178,9 +176,11 @@ export const FormItemMixin = (superClass) =>
|
|
|
178
176
|
if (this.__fieldNode) {
|
|
179
177
|
// Discard the old field
|
|
180
178
|
this.__unlinkLabelFromField(this.__fieldNode);
|
|
181
|
-
this.__updateRequiredState(false);
|
|
182
179
|
this.__fieldNodeObserver.disconnect();
|
|
180
|
+
this.__fieldNode.removeEventListener('blur', this.__onFieldInteraction);
|
|
181
|
+
this.__fieldNode.removeEventListener('change', this.__onFieldInteraction);
|
|
183
182
|
this.__fieldNode = null;
|
|
183
|
+
this.__isFieldDirty = false;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
const fieldNodes = this.$.contentSlot.assignedElements();
|
|
@@ -191,37 +191,43 @@ Please wrap fields with a <vaadin-custom-field> instead.`,
|
|
|
191
191
|
);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
const newFieldNode = fieldNodes.find((field) =>
|
|
195
|
-
return !!this.__getValidateFunction(field);
|
|
196
|
-
});
|
|
194
|
+
const newFieldNode = fieldNodes.find((field) => field.validate || field.checkValidity);
|
|
197
195
|
if (newFieldNode) {
|
|
198
196
|
this.__fieldNode = newFieldNode;
|
|
199
|
-
this.
|
|
200
|
-
this.
|
|
197
|
+
this.__fieldNode.addEventListener('blur', this.__onFieldInteraction);
|
|
198
|
+
this.__fieldNode.addEventListener('change', this.__onFieldInteraction);
|
|
199
|
+
this.__fieldNodeObserver.observe(this.__fieldNode, {
|
|
200
|
+
attributes: true,
|
|
201
|
+
attributeFilter: ['required', 'invalid'],
|
|
202
|
+
});
|
|
201
203
|
|
|
202
204
|
if (this.__labelNode) {
|
|
203
205
|
this.__linkLabelToField(this.__fieldNode);
|
|
204
206
|
}
|
|
205
207
|
}
|
|
208
|
+
|
|
209
|
+
this.__synchronizeAttributes();
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
/** @private */
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
this.__fieldNode.addEventListener('blur', this.__updateInvalidState);
|
|
213
|
-
this.__fieldNode.addEventListener('change', this.__updateInvalidState);
|
|
214
|
-
} else {
|
|
215
|
-
this.removeAttribute('invalid');
|
|
216
|
-
this.removeAttribute('required');
|
|
217
|
-
this.__fieldNode.removeEventListener('blur', this.__updateInvalidState);
|
|
218
|
-
this.__fieldNode.removeEventListener('change', this.__updateInvalidState);
|
|
219
|
-
}
|
|
213
|
+
__onFieldInteraction() {
|
|
214
|
+
this.__isFieldDirty = true;
|
|
215
|
+
this.__synchronizeAttributes();
|
|
220
216
|
}
|
|
221
217
|
|
|
222
218
|
/** @private */
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
219
|
+
__synchronizeAttributes() {
|
|
220
|
+
const field = this.__fieldNode;
|
|
221
|
+
if (!field) {
|
|
222
|
+
this.removeAttribute('required');
|
|
223
|
+
this.removeAttribute('invalid');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.toggleAttribute('required', field.hasAttribute('required'));
|
|
228
|
+
this.toggleAttribute(
|
|
229
|
+
'invalid',
|
|
230
|
+
field.hasAttribute('invalid') || (field.matches(':invalid') && this.__isFieldDirty),
|
|
231
|
+
);
|
|
226
232
|
}
|
|
227
233
|
};
|