Skip to content

Commit 870230e

Browse files
authored
refactor: split layout logic into distinct classes (#8770)
1 parent e1af559 commit 870230e

11 files changed

+564
-359
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)