@vaadin/vaadin-themable-mixin 25.0.0-alpha2 → 25.0.0-alpha20

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.
@@ -0,0 +1,94 @@
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 { LumoInjector } from './src/lumo-injector.js';
7
+
8
+ /**
9
+ * @type {Set<string>}
10
+ */
11
+ const registeredProperties = new Set();
12
+
13
+ /**
14
+ * Find enclosing root for given element to gather style rules from.
15
+ *
16
+ * @param {HTMLElement} element
17
+ * @return {DocumentOrShadowRoot}
18
+ */
19
+ function findRoot(element) {
20
+ const root = element.getRootNode();
21
+
22
+ if (root.host && root.host.constructor.version) {
23
+ return findRoot(root.host);
24
+ }
25
+
26
+ return root;
27
+ }
28
+
29
+ /**
30
+ * Mixin for internal use only. Do not use it in custom components.
31
+ *
32
+ * @polymerMixin
33
+ */
34
+ export const LumoInjectionMixin = (superClass) =>
35
+ class LumoInjectionMixinClass extends superClass {
36
+ static finalize() {
37
+ super.finalize();
38
+
39
+ const propName = this.lumoInjectPropName;
40
+
41
+ // Prevent registering same property twice when a class extends
42
+ // another class using this mixin, since `finalize()` is called
43
+ // by LitElement for all superclasses in the prototype chain.
44
+ if (this.is && !registeredProperties.has(propName)) {
45
+ registeredProperties.add(propName);
46
+
47
+ // Initialize custom property for this class with 0 as default
48
+ // so that changing it to 1 would inject styles to instances
49
+ // Use `inherits: true` so that property defined on `<html>`
50
+ // would apply to components instances within shadow roots
51
+ CSS.registerProperty({
52
+ name: propName,
53
+ syntax: '<number>',
54
+ inherits: true,
55
+ initialValue: '0',
56
+ });
57
+ }
58
+ }
59
+
60
+ static get lumoInjectPropName() {
61
+ return `--_lumo-${this.is}-inject`;
62
+ }
63
+
64
+ static get lumoInjector() {
65
+ return {
66
+ includeBaseStyles: false,
67
+ };
68
+ }
69
+
70
+ /** @protected */
71
+ connectedCallback() {
72
+ super.connectedCallback();
73
+
74
+ if (this.isConnected) {
75
+ const root = findRoot(this);
76
+ root.__lumoInjector ||= new LumoInjector(root);
77
+ this.__lumoInjector = root.__lumoInjector;
78
+ this.__lumoInjector.componentConnected(this);
79
+ }
80
+ }
81
+
82
+ /** @protected */
83
+ disconnectedCallback() {
84
+ super.disconnectedCallback();
85
+
86
+ // Check if LumoInjector is defined. It might be unavailable if the component
87
+ // is moved within the DOM during connectedCallback and becomes disconnected
88
+ // before LumoInjector is assigned.
89
+ if (this.__lumoInjector) {
90
+ this.__lumoInjector.componentDisconnected(this);
91
+ this.__lumoInjector = undefined;
92
+ }
93
+ }
94
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/vaadin-themable-mixin",
3
- "version": "25.0.0-alpha2",
3
+ "version": "25.0.0-alpha20",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -22,6 +22,7 @@
22
22
  "files": [
23
23
  "src",
24
24
  "*.d.ts",
25
+ "lumo-injection-mixin.js",
25
26
  "register-styles.js",
26
27
  "vaadin-*.js"
27
28
  ],
@@ -32,15 +33,15 @@
32
33
  ],
33
34
  "dependencies": {
34
35
  "@open-wc/dedupe-mixin": "^1.3.0",
35
- "lit": "^3.0.0",
36
- "style-observer": "^0.0.8"
36
+ "@vaadin/component-base": "25.0.0-alpha20",
37
+ "lit": "^3.0.0"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@polymer/polymer": "^3.0.0",
40
- "@vaadin/chai-plugins": "25.0.0-alpha2",
41
- "@vaadin/test-runner-commands": "25.0.0-alpha2",
41
+ "@vaadin/chai-plugins": "25.0.0-alpha20",
42
+ "@vaadin/test-runner-commands": "25.0.0-alpha20",
42
43
  "@vaadin/testing-helpers": "^2.0.0",
43
- "sinon": "^18.0.0"
44
+ "sinon": "^21.0.0"
44
45
  },
45
- "gitHead": "67ffcd5355cf21ce1b5039c598525109fc4c164b"
46
+ "gitHead": "c948aae591a30b432f3784000d4677674cae56e0"
46
47
  }
@@ -0,0 +1,81 @@
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
+ * WARNING: For internal use only. Do not use this class in custom components.
9
+ *
10
+ * @private
11
+ */
12
+ export class CSSPropertyObserver {
13
+ #root;
14
+ #callback;
15
+ #properties = new Set();
16
+ #styleSheet;
17
+ #isConnected = false;
18
+
19
+ constructor(root, callback) {
20
+ this.#root = root;
21
+ this.#callback = callback;
22
+ this.#styleSheet = new CSSStyleSheet();
23
+ }
24
+
25
+ #handleTransitionEvent(event) {
26
+ const { propertyName } = event;
27
+ if (this.#properties.has(propertyName)) {
28
+ this.#callback(propertyName);
29
+ }
30
+ }
31
+
32
+ observe(property) {
33
+ this.connect();
34
+
35
+ if (this.#properties.has(property)) {
36
+ return;
37
+ }
38
+
39
+ this.#properties.add(property);
40
+
41
+ this.#styleSheet.replaceSync(`
42
+ :is(:root, :host)::before {
43
+ content: '' !important;
44
+ position: absolute !important;
45
+ top: -9999px !important;
46
+ left: -9999px !important;
47
+ visibility: hidden !important;
48
+ transition: 1ms allow-discrete step-end !important;
49
+ transition-property: ${[...this.#properties].join(', ')} !important;
50
+ }
51
+ `);
52
+ }
53
+
54
+ connect() {
55
+ if (this.#isConnected) {
56
+ return;
57
+ }
58
+
59
+ this.#root.adoptedStyleSheets.unshift(this.#styleSheet);
60
+
61
+ this.#rootHost.addEventListener('transitionstart', (event) => this.#handleTransitionEvent(event));
62
+ this.#rootHost.addEventListener('transitionend', (event) => this.#handleTransitionEvent(event));
63
+
64
+ this.#isConnected = true;
65
+ }
66
+
67
+ disconnect() {
68
+ this.#properties.clear();
69
+
70
+ this.#root.adoptedStyleSheets = this.#root.adoptedStyleSheets.filter((s) => s !== this.#styleSheet);
71
+
72
+ this.#rootHost.removeEventListener('transitionstart', this.#handleTransitionEvent);
73
+ this.#rootHost.removeEventListener('transitionend', this.#handleTransitionEvent);
74
+
75
+ this.#isConnected = false;
76
+ }
77
+
78
+ get #rootHost() {
79
+ return this.#root.documentElement ?? this.#root.host;
80
+ }
81
+ }
package/src/css-utils.js CHANGED
@@ -15,14 +15,14 @@ import { adoptStyles } from 'lit';
15
15
  * @return {CSSStyleSheet[]}
16
16
  */
17
17
  function getEffectiveStyles(component) {
18
- const componentClass = component.constructor;
18
+ const { baseStyles, themeStyles, elementStyles, lumoInjector } = component.constructor;
19
+ const lumoStyleSheet = component.__lumoStyleSheet;
19
20
 
20
- const styleSheet = component.__cssInjectorStyleSheet;
21
- if (styleSheet) {
22
- return [...componentClass.baseStyles, styleSheet, ...componentClass.themeStyles];
21
+ if (lumoStyleSheet && (baseStyles || themeStyles)) {
22
+ return [...(lumoInjector.includeBaseStyles ? baseStyles : []), lumoStyleSheet, ...themeStyles];
23
23
  }
24
24
 
25
- return componentClass.elementStyles;
25
+ return [lumoStyleSheet, ...elementStyles].filter(Boolean);
26
26
  }
27
27
 
28
28
  /**
@@ -31,10 +31,6 @@ function getEffectiveStyles(component) {
31
31
  * @param {HTMLElement} component
32
32
  */
33
33
  export function applyInstanceStyles(component) {
34
- // The adoptStyles function may fall back to appending style elements to shadow root.
35
- // Remove them first to avoid duplicates.
36
- [...component.shadowRoot.querySelectorAll('style')].forEach((style) => style.remove());
37
-
38
34
  adoptStyles(component.shadowRoot, getEffectiveStyles(component));
39
35
  }
40
36
 
@@ -47,23 +43,19 @@ export function applyInstanceStyles(component) {
47
43
  * @param {HTMLElement} component
48
44
  * @param {CSSStyleSheet} styleSheet
49
45
  */
50
- export function injectStyleSheet(component, styleSheet) {
46
+ export function injectLumoStyleSheet(component, styleSheet) {
51
47
  // Store the new stylesheet so that it can be removed later.
52
- component.__cssInjectorStyleSheet = styleSheet;
48
+ component.__lumoStyleSheet = styleSheet;
53
49
  applyInstanceStyles(component);
54
50
  }
55
51
 
56
52
  /**
57
53
  * Removes the stylesheet from the component's shadow root that was added
58
- * by the `injectStyleSheet` function.
54
+ * by the `injectLumoStyleSheet` function.
59
55
  *
60
56
  * @param {HTMLElement} component
61
57
  */
62
- export function cleanupStyleSheet(component) {
63
- const adoptedStyleSheets = component.shadowRoot.adoptedStyleSheets.filter(
64
- (s) => s !== component.__cssInjectorStyleSheet,
65
- );
66
-
67
- component.shadowRoot.adoptedStyleSheets = adoptedStyleSheets;
68
- component.__cssInjectorStyleSheet = undefined;
58
+ export function removeLumoStyleSheet(component) {
59
+ component.__lumoStyleSheet = undefined;
60
+ applyInstanceStyles(component);
69
61
  }
@@ -0,0 +1,172 @@
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 { CSSPropertyObserver } from './css-property-observer.js';
7
+ import { injectLumoStyleSheet, removeLumoStyleSheet } from './css-utils.js';
8
+ import { parseStyleSheets } from './lumo-modules.js';
9
+
10
+ /**
11
+ * Implements auto-injection of CSS styles from document style sheets
12
+ * into the Shadow DOM of corresponding Vaadin components.
13
+ *
14
+ * Styles to be injected are defined as reusable modules using the
15
+ * following syntax, based on media queries and custom properties:
16
+ *
17
+ * ```css
18
+ * \@media lumo_base-field {
19
+ * #label {
20
+ * color: gray;
21
+ * }
22
+ * }
23
+ *
24
+ * \@media lumo_text-field {
25
+ * #input {
26
+ * color: yellow;
27
+ * }
28
+ * }
29
+ *
30
+ * \@media lumo_email-field {
31
+ * #input {
32
+ * color: green;
33
+ * }
34
+ * }
35
+ *
36
+ * html {
37
+ * --_lumo-vaadin-text-field-inject: 1;
38
+ * --_lumo-vaadin-text-field-inject-modules:
39
+ * lumo_base-field,
40
+ * lumo_text-field;
41
+ *
42
+ * --_lumo-vaadin-email-field-inject: 1;
43
+ * --_lumo-vaadin-email-field-inject-modules:
44
+ * lumo_base-field,
45
+ * lumo_email-field;
46
+ * }
47
+ * ```
48
+ *
49
+ * The class observes the custom property `--_lumo-{tagName}-inject`,
50
+ * which indicates whether styles are present for the given component
51
+ * in the document style sheets. When the property is set to `1`, the
52
+ * class recursively searches all document style sheets for CSS modules
53
+ * listed in the `--_lumo-{tagName}-inject-modules` property that apply to
54
+ * the given component tag name. The found rules are then injected
55
+ * into the component's Shadow DOM using the `adoptedStyleSheets` API,
56
+ * in the order specified in the `--_lumo-{tagName}-inject-modules` property.
57
+ * The same module can be used in multiple components.
58
+ *
59
+ * The class also removes the injected styles when the property is set to `0`.
60
+ *
61
+ * If a root element is provided, the class will additionally search for
62
+ * CSS modules in the root element's style sheets. This is useful for
63
+ * embedded Flow applications that are fully isolated from the main document
64
+ * and load styles into a component’s Shadow DOM instead of the main document.
65
+ *
66
+ * WARNING: For internal use only. Do not use this class in custom components.
67
+ *
68
+ * @private
69
+ */
70
+ export class LumoInjector {
71
+ /** @type {Document | ShadowRoot} */
72
+ #root;
73
+
74
+ /** @type {CSSPropertyObserver} */
75
+ #cssPropertyObserver;
76
+
77
+ /** @type {Map<string, CSSStyleSheet>} */
78
+ #styleSheetsByTag = new Map();
79
+
80
+ /** @type {Map<string, Set<HTMLElement>>} */
81
+ #componentsByTag = new Map();
82
+
83
+ constructor(root = document) {
84
+ this.#root = root;
85
+ this.#cssPropertyObserver = new CSSPropertyObserver(this.#root, (propertyName) => {
86
+ const tagName = propertyName.match(/^--_lumo-(.*)-inject$/u)?.[1];
87
+ this.#updateStyleSheet(tagName);
88
+ });
89
+ }
90
+
91
+ disconnect() {
92
+ this.#cssPropertyObserver.disconnect();
93
+ this.#styleSheetsByTag.clear();
94
+ this.#componentsByTag.values().forEach((components) => components.forEach(removeLumoStyleSheet));
95
+ }
96
+
97
+ /**
98
+ * Adds a component to the list of elements monitored for style injection.
99
+ * If the styles have already been detected, they are injected into the
100
+ * component's shadow DOM immediately. Otherwise, the class watches the
101
+ * custom property `--_lumo-{tagName}-inject` to trigger injection when
102
+ * the styles are added to the document or root element.
103
+ *
104
+ * @param {HTMLElement} component
105
+ */
106
+ componentConnected(component) {
107
+ const { is: tagName, lumoInjectPropName } = component.constructor;
108
+
109
+ this.#componentsByTag.set(tagName, this.#componentsByTag.get(tagName) ?? new Set());
110
+ this.#componentsByTag.get(tagName).add(component);
111
+
112
+ const stylesheet = this.#styleSheetsByTag.get(tagName);
113
+ if (stylesheet) {
114
+ if (stylesheet.cssRules.length > 0) {
115
+ injectLumoStyleSheet(component, stylesheet);
116
+ }
117
+ return;
118
+ }
119
+
120
+ this.#initStyleSheet(tagName);
121
+ this.#cssPropertyObserver.observe(lumoInjectPropName);
122
+ }
123
+
124
+ /**
125
+ * Removes the component from the list of elements monitored for
126
+ * style injection and cleans up any previously injected styles.
127
+ *
128
+ * @param {HTMLElement} component
129
+ */
130
+ componentDisconnected(component) {
131
+ const { is: tagName } = component.constructor;
132
+ this.#componentsByTag.get(tagName)?.delete(component);
133
+
134
+ removeLumoStyleSheet(component);
135
+ }
136
+
137
+ #initStyleSheet(tagName) {
138
+ this.#styleSheetsByTag.set(tagName, new CSSStyleSheet());
139
+ this.#updateStyleSheet(tagName);
140
+ }
141
+
142
+ #updateStyleSheet(tagName) {
143
+ const { tags, modules } = parseStyleSheets(this.#rootStyleSheets);
144
+
145
+ const cssText = (tags.get(tagName) ?? [])
146
+ .flatMap((moduleName) => modules.get(moduleName) ?? [])
147
+ .map((rule) => rule.cssText)
148
+ .join('\n');
149
+
150
+ const stylesheet = this.#styleSheetsByTag.get(tagName);
151
+ stylesheet.replaceSync(cssText);
152
+
153
+ this.#componentsByTag.get(tagName)?.forEach((component) => {
154
+ if (cssText) {
155
+ injectLumoStyleSheet(component, stylesheet);
156
+ } else {
157
+ removeLumoStyleSheet(component);
158
+ }
159
+ });
160
+ }
161
+
162
+ get #rootStyleSheets() {
163
+ let styleSheets = new Set();
164
+
165
+ for (const root of [this.#root, document]) {
166
+ styleSheets = styleSheets.union(new Set(root.styleSheets));
167
+ styleSheets = styleSheets.union(new Set(root.adoptedStyleSheets));
168
+ }
169
+
170
+ return [...styleSheets];
171
+ }
172
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { issueWarning } from '@vaadin/component-base/src/warnings.js';
7
+
8
+ /** @type {WeakMap<CSSStyleSheet, Record<string, Map>>} */
9
+ const cache = new WeakMap();
10
+
11
+ function getRuleMediaText(rule) {
12
+ try {
13
+ return rule.media.mediaText;
14
+ } catch {
15
+ issueWarning(
16
+ '[LumoInjector] Browser denied to access property "mediaText" for some CSS rules, so they were skipped.',
17
+ );
18
+ return '';
19
+ }
20
+ }
21
+
22
+ function getStyleSheetRules(styleSheet) {
23
+ try {
24
+ return styleSheet.cssRules;
25
+ } catch {
26
+ issueWarning(
27
+ '[LumoInjector] Browser denied to access property "cssRules" for some CSS stylesheets, so they were skipped.',
28
+ );
29
+ return [];
30
+ }
31
+ }
32
+
33
+ function parseStyleSheet(
34
+ styleSheet,
35
+ result = {
36
+ tags: new Map(),
37
+ modules: new Map(),
38
+ },
39
+ ) {
40
+ for (const rule of getStyleSheetRules(styleSheet)) {
41
+ if (rule instanceof CSSImportRule) {
42
+ const mediaText = getRuleMediaText(rule);
43
+ if (mediaText.startsWith('lumo_')) {
44
+ result.modules.set(mediaText, [...rule.styleSheet.cssRules]);
45
+ } else {
46
+ parseStyleSheet(rule.styleSheet, result);
47
+ }
48
+
49
+ continue;
50
+ }
51
+
52
+ if (rule instanceof CSSMediaRule) {
53
+ const mediaText = getRuleMediaText(rule);
54
+ if (mediaText.startsWith('lumo_')) {
55
+ result.modules.set(mediaText, [...rule.cssRules]);
56
+ }
57
+
58
+ continue;
59
+ }
60
+
61
+ if (rule instanceof CSSStyleRule && rule.cssText.includes('-inject')) {
62
+ for (const property of rule.style) {
63
+ const tagName = property.match(/^--_lumo-(.*)-inject-modules$/u)?.[1];
64
+ if (!tagName) {
65
+ continue;
66
+ }
67
+
68
+ const value = rule.style.getPropertyValue(property);
69
+
70
+ result.tags.set(
71
+ tagName,
72
+ value.split(',').map((module) => module.trim().replace(/'|"/gu, '')),
73
+ );
74
+ }
75
+
76
+ continue;
77
+ }
78
+ }
79
+
80
+ return result;
81
+ }
82
+
83
+ /**
84
+ * Parses the provided CSSStyleSheet objects and returns an object with
85
+ * tag-to-modules mappings and module rules.
86
+ *
87
+ * Modules are defined using CSS media rules with names starting with `lumo_`:
88
+ *
89
+ * ```css
90
+ * \@media lumo_base-field {
91
+ * #label {
92
+ * color: gray;
93
+ * }
94
+ * }
95
+ *
96
+ * \@media lumo_text-field {
97
+ * #input {
98
+ * color: yellow;
99
+ * }
100
+ * }
101
+ * ```
102
+ *
103
+ * Also, an entire CSS import can be defined as a module:
104
+ *
105
+ * ```css
106
+ * \@import 'lumo-base-field.css' lumo_base-field;
107
+ * ```
108
+ *
109
+ * Tag-to-modules mappings are defined as CSS custom properties that list
110
+ * the module names to be applied to specific tags, e.g. `vaadin-text-field`:
111
+ *
112
+ * ```css
113
+ * html {
114
+ * --_lumo-vaadin-text-field-inject-modules:
115
+ * lumo_base-field,
116
+ * lumo_text-field;
117
+ * }
118
+ * ```
119
+ *
120
+ * Example output:
121
+ *
122
+ * ```js
123
+ * {
124
+ * tags: Map {
125
+ * 'vaadin-text-field': ['lumo_base-field', 'lumo_text-field']
126
+ * },
127
+ * modules: Map {
128
+ * 'lumo_base-field': [CSSStyleRule],
129
+ * 'lumo_text-field': [CSSStyleRule]
130
+ * }
131
+ * }
132
+ * ```
133
+ *
134
+ * @param {CSSStyleSheet[]} styleSheets - An array of CSSStyleSheet objects to parse.
135
+ * @return {{tags: Map<string, string[]>, modules: Map<string, CSSRule[]>}}
136
+ */
137
+ export function parseStyleSheets(styleSheets) {
138
+ let tags = new Map();
139
+ let modules = new Map();
140
+
141
+ for (const styleSheet of styleSheets) {
142
+ let result = cache.get(styleSheet);
143
+ if (!result) {
144
+ result = parseStyleSheet(styleSheet);
145
+ cache.set(styleSheet, result);
146
+ }
147
+
148
+ tags = new Map([...tags, ...result.tags]);
149
+ modules = new Map([...modules, ...result.modules]);
150
+ }
151
+
152
+ return { tags, modules };
153
+ }
@@ -7,9 +7,6 @@ import type { Constructor } from '@open-wc/dedupe-mixin';
7
7
  import type { CSSResult, CSSResultGroup } from 'lit';
8
8
  import type { ThemePropertyMixinClass } from './vaadin-theme-property-mixin.js';
9
9
 
10
- /**
11
- * A mixin for `nav` elements, facilitating navigation and selection of childNodes.
12
- */
13
10
  export declare function ThemableMixin<T extends Constructor<HTMLElement>>(
14
11
  base: T,
15
12
  ): Constructor<ThemableMixinClass> & Constructor<ThemePropertyMixinClass> & T;
@@ -29,18 +26,6 @@ export declare interface ThemableMixinClass extends ThemePropertyMixinClass {}
29
26
  */
30
27
  declare function registerStyles(themeFor: string | null, styles: CSSResultGroup, options?: object | null): void;
31
28
 
32
- type Theme = {
33
- themeFor: string;
34
- styles: CSSResult[];
35
- moduleId?: string;
36
- include?: string[] | string;
37
- };
38
-
39
- /**
40
- * For internal purposes only.
41
- */
42
- declare const __themeRegistry: Theme[];
43
-
44
29
  export { css, unsafeCSS } from 'lit';
45
30
 
46
- export { registerStyles, __themeRegistry };
31
+ export { registerStyles };
@@ -55,7 +55,6 @@ function hasThemes(tagName) {
55
55
  /**
56
56
  * Flattens the styles into a single array of styles.
57
57
  * @param {CSSResultGroup} styles
58
- * @param {CSSResult[]} result
59
58
  * @returns {CSSResult[]}
60
59
  */
61
60
  function flattenStyles(styles = []) {
@@ -234,18 +233,6 @@ export function registerStyles(themeFor, styles, options = {}) {
234
233
  }
235
234
  }
236
235
 
237
- /**
238
- * Returns all registered themes. By default the themeRegistry is returned as is.
239
- * In case the style-modules adapter is imported, the themes are obtained from there instead
240
- * @returns {Theme[]}
241
- */
242
- function getAllThemes() {
243
- if (window.Vaadin && window.Vaadin.styleModules) {
244
- return window.Vaadin.styleModules.getAllThemes();
245
- }
246
- return themeRegistry;
247
- }
248
-
249
236
  /**
250
237
  * Maps the moduleName to an include priority number which is used for
251
238
  * determining the order in which styles are applied.
@@ -271,7 +258,7 @@ function getIncludedStyles(theme) {
271
258
  const includedStyles = [];
272
259
  if (theme.include) {
273
260
  [].concat(theme.include).forEach((includeModuleId) => {
274
- const includedTheme = getAllThemes().find((s) => s.moduleId === includeModuleId);
261
+ const includedTheme = themeRegistry.find((s) => s.moduleId === includeModuleId);
275
262
  if (includedTheme) {
276
263
  includedStyles.push(...getIncludedStyles(includedTheme), ...includedTheme.styles);
277
264
  } else {
@@ -291,7 +278,7 @@ function getIncludedStyles(theme) {
291
278
  function getThemes(tagName) {
292
279
  const defaultModuleName = `${tagName}-default-theme`;
293
280
 
294
- const themes = getAllThemes()
281
+ const themes = themeRegistry
295
282
  // Filter by matching themeFor properties
296
283
  .filter((theme) => theme.moduleId !== defaultModuleName && matchesThemeFor(theme.themeFor, tagName))
297
284
  .map((theme) => ({
@@ -308,7 +295,7 @@ function getThemes(tagName) {
308
295
  return themes;
309
296
  }
310
297
  // No theme modules found, return the default module if it exists
311
- return getAllThemes().filter((theme) => theme.moduleId === defaultModuleName);
298
+ return themeRegistry.filter((theme) => theme.moduleId === defaultModuleName);
312
299
  }
313
300
 
314
301
  /**
@@ -354,11 +341,11 @@ export const ThemableMixin = (superClass) =>
354
341
  */
355
342
  static finalizeStyles(styles) {
356
343
  // Preserve the styles the user supplied via the `static get styles()` getter
357
- // so that they will always be injected before styles added by `CSSInjector`.
344
+ // so that they will always be injected before styles added by `LumoInjector`.
358
345
  this.baseStyles = styles ? [styles].flat(Infinity) : [];
359
346
 
360
347
  // Preserve the theme styles the user supplied via the `registerStyles()` API
361
- // so that they will always be injected after styles added by `CSSInjector`.
348
+ // so that they will always be injected after styles added by `LumoInjector`.
362
349
  this.themeStyles = this.getStylesForThis();
363
350
 
364
351
  // Merged styles are stored in `elementStyles` and passed to `adoptStyles()`.
@@ -380,5 +367,3 @@ export const ThemableMixin = (superClass) =>
380
367
  return themeStyles.filter((style, index) => index === themeStyles.lastIndexOf(style));
381
368
  }
382
369
  };
383
-
384
- export { themeRegistry as __themeRegistry };
@@ -1,154 +0,0 @@
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
- /* eslint-disable es/no-optional-chaining */
8
- import StyleObserver from 'style-observer';
9
- import { extractTagScopedCSSRules } from './css-rules.js';
10
- import { cleanupStyleSheet, injectStyleSheet } from './css-utils.js';
11
-
12
- /**
13
- * Implements auto-injection of component-scoped CSS styles from document
14
- * style sheets into the Shadow DOM of the corresponding Vaadin components.
15
- *
16
- * Styles are scoped to a component using the following syntax:
17
- *
18
- * 1. `@media vaadin-text-field { ... }` - a media query with a tag name
19
- * 2. `@import "styles.css" vaadin-text-field` - an import rule with a tag name
20
- *
21
- * The class observes the custom property `--{tagName}-css-inject`,
22
- * which indicates the presence of styles for the given component in
23
- * the document style sheets. When the property is set to `1`, the class
24
- * recursively searches all document style sheets for any CSS rules that
25
- * are scoped to the given component tag name using the syntax described
26
- * above. The found rules are then injected into the shadow DOM of all
27
- * subscribed components through the adoptedStyleSheets API.
28
- *
29
- * The class also observes the custom property to remove the styles when
30
- * the property is set to `0`.
31
- *
32
- * If a root element is provided, the class will additionally search for
33
- * component-scoped styles in the root element's style sheets. This is
34
- * useful for embedded Flow applications that are fully isolated from
35
- * the main document and load styles into a component's shadow DOM
36
- * rather than the main document.
37
- *
38
- * WARNING: For internal use only. Do not use this class in custom components.
39
- */
40
- export class CSSInjector {
41
- /** @type {Document | ShadowRoot} */
42
- #root;
43
-
44
- /** @type {Map<string, HTMLElement[]>} */
45
- #componentsByTag = new Map();
46
-
47
- /** @type {Map<string, CSSStyleSheet>} */
48
- #styleSheetsByTag = new Map();
49
-
50
- #styleObserver = new StyleObserver((records) => {
51
- records.forEach((record) => {
52
- const { property, value, oldValue } = record;
53
- const tagName = property.slice(2).replace('-css-inject', '');
54
- if (value === '1') {
55
- this.#componentStylesAdded(tagName);
56
- } else if (oldValue === '1') {
57
- this.#componentStylesRemoved(tagName);
58
- }
59
- });
60
- });
61
-
62
- constructor(root = document) {
63
- this.#root = root;
64
- }
65
-
66
- /**
67
- * Adds a component to the list of elements monitored for component-scoped
68
- * styles in global style sheets. If the styles have already been detected,
69
- * they are injected into the component's shadow DOM immediately. Otherwise,
70
- * the class watches the custom property `--{tagName}-css-inject` to trigger
71
- * injection when the styles are added to the document or root element.
72
- *
73
- * @param {HTMLElement} component
74
- */
75
- componentConnected(component) {
76
- const { is: tagName, cssInjectPropName } = component.constructor;
77
-
78
- if (this.#componentsByTag.has(tagName)) {
79
- this.#componentsByTag.get(tagName).add(component);
80
- } else {
81
- this.#componentsByTag.set(tagName, new Set([component]));
82
- }
83
-
84
- const stylesheet = this.#styleSheetsByTag.get(tagName);
85
- if (stylesheet) {
86
- injectStyleSheet(component, stylesheet);
87
- return;
88
- }
89
-
90
- // If styles for custom property are already loaded for this root,
91
- // store corresponding tag name so that we can inject styles
92
- const value = getComputedStyle(this.#rootHost).getPropertyValue(cssInjectPropName);
93
- if (value === '1') {
94
- this.#componentStylesAdded(tagName);
95
- }
96
-
97
- // Observe custom property that would trigger injection for this class
98
- this.#styleObserver.observe(this.#rootHost, cssInjectPropName);
99
- }
100
-
101
- /**
102
- * Removes the component from the list of elements monitored for
103
- * component-scoped styles and cleans up any previously injected
104
- * styles from the component's shadow DOM.
105
- *
106
- * @param {HTMLElement} component
107
- */
108
- componentDisconnected(component) {
109
- const { is: tagName } = component.constructor;
110
-
111
- cleanupStyleSheet(component);
112
-
113
- this.#componentsByTag.get(tagName)?.delete(component);
114
- }
115
-
116
- #componentStylesAdded(tagName) {
117
- const stylesheet = this.#styleSheetsByTag.get(tagName) || new CSSStyleSheet();
118
-
119
- const cssText = this.#extractComponentScopedCSSRules(tagName)
120
- .map((rule) => rule.cssText)
121
- .join('\n');
122
- stylesheet.replaceSync(cssText);
123
-
124
- this.#componentsByTag.get(tagName)?.forEach((component) => {
125
- injectStyleSheet(component, stylesheet);
126
- });
127
-
128
- this.#styleSheetsByTag.set(tagName, stylesheet);
129
- }
130
-
131
- #componentStylesRemoved(tagName) {
132
- this.#componentsByTag.get(tagName)?.forEach((component) => {
133
- cleanupStyleSheet(component);
134
- });
135
-
136
- this.#styleSheetsByTag.delete(tagName);
137
- }
138
-
139
- #extractComponentScopedCSSRules(tagName) {
140
- // Global stylesheets
141
- const rules = extractTagScopedCSSRules(document, tagName);
142
-
143
- // Scoped stylesheets
144
- if (this.#root !== document) {
145
- rules.push(...extractTagScopedCSSRules(this.#root, tagName));
146
- }
147
-
148
- return rules;
149
- }
150
-
151
- get #rootHost() {
152
- return this.#root === document ? this.#root.documentElement : this.#root.host;
153
- }
154
- }
package/src/css-rules.js DELETED
@@ -1,90 +0,0 @@
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
- // Based on https://github.com/jouni/j-elements/blob/main/test/old-components/Stylable.js
8
-
9
- /**
10
- * Check if the media query is a non-standard "tag scoped selector".
11
- *
12
- * Examples of such media queries:
13
- * - `@media vaadin-text-field { ... }`
14
- * - `@import "styles.css" vaadin-text-field`.
15
- *
16
- * @param {string} media
17
- * @return {boolean}
18
- */
19
- function isTagScopedMedia(media) {
20
- return /^\w+(-\w+)+$/u.test(media);
21
- }
22
-
23
- /**
24
- * Check if the media query string matches the given tag name.
25
- *
26
- * @param {string} media
27
- * @param {string} tagName
28
- * @return {boolean}
29
- */
30
- function matchesTagScopedMedia(media, tagName) {
31
- return media === tagName;
32
- }
33
-
34
- /**
35
- * Recursively processes a style sheet for matching "tag scoped" rules.
36
- *
37
- * @param {CSSStyleSheet} styleSheet
38
- * @param {string} tagName
39
- */
40
- function extractStyleSheetTagScopedCSSRules(styleSheet, tagName) {
41
- const matchingRules = [];
42
-
43
- for (const rule of styleSheet.cssRules) {
44
- const ruleType = rule.constructor.name;
45
-
46
- if (ruleType === 'CSSImportRule') {
47
- if (!isTagScopedMedia(rule.media.mediaText)) {
48
- matchingRules.push(...extractStyleSheetTagScopedCSSRules(rule.styleSheet, tagName));
49
- continue;
50
- }
51
-
52
- if (matchesTagScopedMedia(rule.media.mediaText, tagName)) {
53
- matchingRules.push(...rule.styleSheet.cssRules);
54
- }
55
- }
56
-
57
- if (ruleType === 'CSSMediaRule') {
58
- if (matchesTagScopedMedia(rule.media.mediaText, tagName)) {
59
- matchingRules.push(...rule.cssRules);
60
- }
61
- }
62
- }
63
-
64
- return matchingRules;
65
- }
66
-
67
- /**
68
- * Recursively processes style sheets of the specified root element, including both
69
- * `adoptedStyleSheets` and regular `styleSheets`, and returns all CSS rules from
70
- * `@media` and `@import` blocks where the media query is (a) "tag scoped selector",
71
- * and (b) matches the specified tag name.
72
- *
73
- * Examples of such media queries:
74
- * - `@media vaadin-text-field { ... }`
75
- * - `@import "styles.css" vaadin-text-field`
76
- *
77
- * The returned rules are ordered in the same way as they are in the original stylesheet.
78
- *
79
- * @param {DocumentOrShadowRoot} root
80
- * @param {string} tagName
81
- * @return {CSSRule[]}
82
- */
83
- export function extractTagScopedCSSRules(root, tagName) {
84
- const styleSheets = new Set([...root.styleSheets]);
85
- const adoptedStyleSheets = new Set([...root.adoptedStyleSheets]);
86
-
87
- return [...styleSheets.union(adoptedStyleSheets)].flatMap((styleSheet) => {
88
- return extractStyleSheetTagScopedCSSRules(styleSheet, tagName);
89
- });
90
- }