Skip to content

Commit dc923df

Browse files
authored
refactor: set adoptedStyleSheets only once per component connection (#9660)
1 parent e024063 commit dc923df

File tree

3 files changed

+80
-17
lines changed

3 files changed

+80
-17
lines changed

packages/vaadin-themable-mixin/src/css-property-observer.js

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,36 @@ export class CSSPropertyObserver {
1414
#name;
1515
#callback;
1616
#properties = new Set();
17+
#styleSheet;
18+
#isConnected = false;
1719

1820
constructor(root, name, callback) {
1921
this.#root = root;
2022
this.#name = name;
2123
this.#callback = callback;
24+
}
25+
26+
#handleTransitionEvent(event) {
27+
const { propertyName } = event;
28+
if (this.#properties.has(propertyName)) {
29+
this.#callback(propertyName);
30+
}
31+
}
32+
33+
observe(property) {
34+
this.connect();
35+
36+
this.#properties.add(property);
37+
this.#rootHost.style.setProperty(`--${this.#name}-props`, [...this.#properties].join(', '));
38+
}
2239

23-
const styleSheet = new CSSStyleSheet();
24-
styleSheet.replaceSync(`
40+
connect() {
41+
if (this.#isConnected) {
42+
return;
43+
}
44+
45+
this.#styleSheet = new CSSStyleSheet();
46+
this.#styleSheet.replaceSync(`
2547
:is(:root, :host)::before {
2648
content: '' !important;
2749
position: absolute !important;
@@ -32,22 +54,24 @@ export class CSSPropertyObserver {
3254
transition-property: var(--${this.#name}-props) !important;
3355
}
3456
`);
35-
this.#root.adoptedStyleSheets.unshift(styleSheet);
57+
this.#root.adoptedStyleSheets.unshift(this.#styleSheet);
3658

3759
this.#rootHost.addEventListener('transitionstart', (event) => this.#handleTransitionEvent(event));
3860
this.#rootHost.addEventListener('transitionend', (event) => this.#handleTransitionEvent(event));
39-
}
4061

41-
#handleTransitionEvent(event) {
42-
const { propertyName } = event;
43-
if (this.#properties.has(propertyName)) {
44-
this.#callback(propertyName);
45-
}
62+
this.#isConnected = true;
4663
}
4764

48-
observe(property) {
49-
this.#properties.add(property);
50-
this.#rootHost.style.setProperty(`--${this.#name}-props`, [...this.#properties].join(', '));
65+
disconnect() {
66+
this.#properties.clear();
67+
68+
this.#root.adoptedStyleSheets = this.#root.adoptedStyleSheets.filter((s) => s !== this.#styleSheet);
69+
70+
this.#rootHost.removeEventListener('transitionstart', this.#handleTransitionEvent);
71+
this.#rootHost.removeEventListener('transitionend', this.#handleTransitionEvent);
72+
this.#rootHost.style.removeProperty(`--${this.#name}-props`);
73+
74+
this.#isConnected = false;
5175
}
5276

5377
get #rootHost() {

packages/vaadin-themable-mixin/src/lumo-injector.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,16 @@ export class LumoInjector {
8484
this.#root = root;
8585
this.#cssPropertyObserver = new CSSPropertyObserver(this.#root, 'vaadin-lumo-injector', (propertyName) => {
8686
const tagName = propertyName.slice(2).replace('-lumo-inject', '');
87-
this.#updateComponentStyleSheet(tagName);
87+
this.#updateStyleSheet(tagName);
8888
});
8989
}
9090

91+
disconnect() {
92+
this.#cssPropertyObserver.disconnect();
93+
this.#styleSheetsByTag.clear();
94+
this.#componentsByTag.values().forEach((components) => components.forEach(removeLumoStyleSheet));
95+
}
96+
9197
/**
9298
* Adds a component to the list of elements monitored for style injection.
9399
* If the styles have already been detected, they are injected into the
@@ -103,8 +109,15 @@ export class LumoInjector {
103109
this.#componentsByTag.set(tagName, this.#componentsByTag.get(tagName) ?? new Set());
104110
this.#componentsByTag.get(tagName).add(component);
105111

106-
this.#updateComponentStyleSheet(tagName);
112+
const stylesheet = this.#styleSheetsByTag.get(tagName);
113+
if (stylesheet) {
114+
if (stylesheet.cssRules.length > 0) {
115+
injectLumoStyleSheet(component, stylesheet);
116+
}
117+
return;
118+
}
107119

120+
this.#initStyleSheet(tagName);
108121
this.#cssPropertyObserver.observe(lumoInjectPropName);
109122
}
110123

@@ -121,17 +134,21 @@ export class LumoInjector {
121134
removeLumoStyleSheet(component);
122135
}
123136

124-
#updateComponentStyleSheet(tagName) {
137+
#initStyleSheet(tagName) {
138+
this.#styleSheetsByTag.set(tagName, new CSSStyleSheet());
139+
this.#updateStyleSheet(tagName);
140+
}
141+
142+
#updateStyleSheet(tagName) {
125143
const { tags, modules } = parseStyleSheets(this.#rootStyleSheets);
126144

127145
const cssText = (tags.get(tagName) ?? [])
128146
.flatMap((moduleName) => modules.get(moduleName) ?? [])
129147
.map((rule) => rule.cssText)
130148
.join('\n');
131149

132-
const stylesheet = this.#styleSheetsByTag.get(tagName) ?? new CSSStyleSheet();
150+
const stylesheet = this.#styleSheetsByTag.get(tagName);
133151
stylesheet.replaceSync(cssText);
134-
this.#styleSheetsByTag.set(tagName, stylesheet);
135152

136153
this.#componentsByTag.get(tagName)?.forEach((component) => {
137154
if (cssText) {

packages/vaadin-themable-mixin/test/lumo-injection-mixin.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ describe('Lumo injection', () => {
117117
}
118118

119119
describe('in global scope', () => {
120+
afterEach(() => {
121+
document.__lumoInjector?.disconnect();
122+
document.__lumoInjector = undefined;
123+
});
124+
120125
describe('styles added after element is connected', () => {
121126
beforeEach(async () => {
122127
element = fixtureSync('<test-foo></test-foo>');
@@ -237,6 +242,11 @@ describe('Lumo injection', () => {
237242
element = document.createElement('test-foo');
238243
});
239244

245+
afterEach(() => {
246+
host.__lumoInjector?.disconnect();
247+
host.__lumoInjector = undefined;
248+
});
249+
240250
describe('styles added after element is connected', () => {
241251
beforeEach(async () => {
242252
host.shadowRoot.appendChild(element);
@@ -413,6 +423,11 @@ describe('Lumo injection', () => {
413423
wrapper = document.createElement('test-bar');
414424
});
415425

426+
afterEach(() => {
427+
host.__lumoInjector?.disconnect();
428+
host.__lumoInjector = undefined;
429+
});
430+
416431
it('should inject matching styles added to parent shadow host', async () => {
417432
host.shadowRoot.appendChild(wrapper);
418433
await nextRender();
@@ -447,6 +462,11 @@ describe('Lumo injection', () => {
447462
content = element.shadowRoot.querySelector('[part="content"]');
448463
});
449464

465+
afterEach(() => {
466+
document.__lumoInjector?.disconnect();
467+
document.__lumoInjector = undefined;
468+
});
469+
450470
it('should inject matching styles for the extending component', async () => {
451471
const style = document.createElement('style');
452472
style.textContent = TEST_FOO_STYLES.replaceAll('foo', 'baz');
@@ -483,6 +503,8 @@ describe('Lumo injection', () => {
483503

484504
afterEach(() => {
485505
style.remove();
506+
document.__lumoInjector?.disconnect();
507+
document.__lumoInjector = undefined;
486508
});
487509

488510
it('should not remove styles from injected stylesheets when calling registerStyles()', () => {

0 commit comments

Comments
 (0)