@vaadin/vaadin-themable-mixin 25.0.0-alpha4 → 25.0.0-alpha6

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.
@@ -3,7 +3,7 @@
3
3
  * Copyright (c) 2021 - 2025 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import { CSSInjector } from './src/css-injector.js';
6
+ import { LumoInjector } from './src/lumo-injector.js';
7
7
 
8
8
  /**
9
9
  * @type {Set<string>}
@@ -31,12 +31,12 @@ function findRoot(element) {
31
31
  *
32
32
  * @polymerMixin
33
33
  */
34
- export const CSSInjectionMixin = (superClass) =>
35
- class CSSInjectionMixinClass extends superClass {
34
+ export const LumoInjectionMixin = (superClass) =>
35
+ class LumoInjectionMixinClass extends superClass {
36
36
  static finalize() {
37
37
  super.finalize();
38
38
 
39
- const propName = this.cssInjectPropName;
39
+ const propName = this.lumoInjectPropName;
40
40
 
41
41
  // Prevent registering same property twice when a class extends
42
42
  // another class using this mixin, since `finalize()` is called
@@ -57,8 +57,8 @@ export const CSSInjectionMixin = (superClass) =>
57
57
  }
58
58
  }
59
59
 
60
- static get cssInjectPropName() {
61
- return `--${this.is}-css-inject`;
60
+ static get lumoInjectPropName() {
61
+ return `--${this.is}-lumo-inject`;
62
62
  }
63
63
 
64
64
  /** @protected */
@@ -67,9 +67,9 @@ export const CSSInjectionMixin = (superClass) =>
67
67
 
68
68
  if (this.isConnected) {
69
69
  const root = findRoot(this);
70
- root.__cssInjector ||= new CSSInjector(root);
71
- this.__cssInjector = root.__cssInjector;
72
- this.__cssInjector.componentConnected(this);
70
+ root.__lumoInjector ||= new LumoInjector(root);
71
+ this.__lumoInjector = root.__lumoInjector;
72
+ this.__lumoInjector.componentConnected(this);
73
73
  }
74
74
  }
75
75
 
@@ -77,12 +77,12 @@ export const CSSInjectionMixin = (superClass) =>
77
77
  disconnectedCallback() {
78
78
  super.disconnectedCallback();
79
79
 
80
- // Check if CSSInjector is defined. It might be unavailable if the component
80
+ // Check if LumoInjector is defined. It might be unavailable if the component
81
81
  // is moved within the DOM during connectedCallback and becomes disconnected
82
- // before CSSInjector is assigned.
83
- if (this.__cssInjector) {
84
- this.__cssInjector.componentDisconnected(this);
85
- this.__cssInjector = undefined;
82
+ // before LumoInjector is assigned.
83
+ if (this.__lumoInjector) {
84
+ this.__lumoInjector.componentDisconnected(this);
85
+ this.__lumoInjector = undefined;
86
86
  }
87
87
  }
88
88
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/vaadin-themable-mixin",
3
- "version": "25.0.0-alpha4",
3
+ "version": "25.0.0-alpha6",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -22,7 +22,7 @@
22
22
  "files": [
23
23
  "src",
24
24
  "*.d.ts",
25
- "css-injection-mixin.js",
25
+ "lumo-injection-mixin.js",
26
26
  "register-styles.js",
27
27
  "vaadin-*.js"
28
28
  ],
@@ -37,10 +37,10 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@polymer/polymer": "^3.0.0",
40
- "@vaadin/chai-plugins": "25.0.0-alpha4",
41
- "@vaadin/test-runner-commands": "25.0.0-alpha4",
40
+ "@vaadin/chai-plugins": "25.0.0-alpha6",
41
+ "@vaadin/test-runner-commands": "25.0.0-alpha6",
42
42
  "@vaadin/testing-helpers": "^2.0.0",
43
43
  "sinon": "^18.0.0"
44
44
  },
45
- "gitHead": "ce4421f0daf26027b863b91787a474e4cc264344"
45
+ "gitHead": "cd1d084198d2b326c58d44bb39fa4845b71ce551"
46
46
  }
@@ -23,13 +23,13 @@ export class CSSPropertyObserver {
23
23
  const styleSheet = new CSSStyleSheet();
24
24
  styleSheet.replaceSync(`
25
25
  :is(:root, :host)::before {
26
- content: '';
27
- position: absolute;
28
- top: -9999px;
29
- left: -9999px;
30
- visibility: hidden;
31
- transition: 1ms allow-discrete step-end;
32
- transition-property: var(--${this.#name}-props);
26
+ content: '' !important;
27
+ position: absolute !important;
28
+ top: -9999px !important;
29
+ left: -9999px !important;
30
+ visibility: hidden !important;
31
+ transition: 1ms allow-discrete step-end !important;
32
+ transition-property: var(--${this.#name}-props) !important;
33
33
  }
34
34
  `);
35
35
  this.#root.adoptedStyleSheets.unshift(styleSheet);
package/src/css-utils.js CHANGED
@@ -17,7 +17,7 @@ import { adoptStyles } from 'lit';
17
17
  function getEffectiveStyles(component) {
18
18
  const componentClass = component.constructor;
19
19
 
20
- const styleSheet = component.__cssInjectorStyleSheet;
20
+ const styleSheet = component.__lumoInjectorStyleSheet;
21
21
  if (styleSheet) {
22
22
  return (componentClass.baseStyles ?? componentClass.themeStyles)
23
23
  ? [...componentClass.baseStyles, styleSheet, ...componentClass.themeStyles]
@@ -33,10 +33,6 @@ function getEffectiveStyles(component) {
33
33
  * @param {HTMLElement} component
34
34
  */
35
35
  export function applyInstanceStyles(component) {
36
- // The adoptStyles function may fall back to appending style elements to shadow root.
37
- // Remove them first to avoid duplicates.
38
- [...component.shadowRoot.querySelectorAll('style')].forEach((style) => style.remove());
39
-
40
36
  adoptStyles(component.shadowRoot, getEffectiveStyles(component));
41
37
  }
42
38
 
@@ -49,23 +45,23 @@ export function applyInstanceStyles(component) {
49
45
  * @param {HTMLElement} component
50
46
  * @param {CSSStyleSheet} styleSheet
51
47
  */
52
- export function injectStyleSheet(component, styleSheet) {
48
+ export function injectLumoStyleSheet(component, styleSheet) {
53
49
  // Store the new stylesheet so that it can be removed later.
54
- component.__cssInjectorStyleSheet = styleSheet;
50
+ component.__lumoInjectorStyleSheet = styleSheet;
55
51
  applyInstanceStyles(component);
56
52
  }
57
53
 
58
54
  /**
59
55
  * Removes the stylesheet from the component's shadow root that was added
60
- * by the `injectStyleSheet` function.
56
+ * by the `injectLumoStyleSheet` function.
61
57
  *
62
58
  * @param {HTMLElement} component
63
59
  */
64
- export function cleanupStyleSheet(component) {
60
+ export function cleanupLumoStyleSheet(component) {
65
61
  const adoptedStyleSheets = component.shadowRoot.adoptedStyleSheets.filter(
66
- (s) => s !== component.__cssInjectorStyleSheet,
62
+ (s) => s !== component.__lumoInjectorStyleSheet,
67
63
  );
68
64
 
69
65
  component.shadowRoot.adoptedStyleSheets = adoptedStyleSheets;
70
- component.__cssInjectorStyleSheet = undefined;
66
+ component.__lumoInjectorStyleSheet = undefined;
71
67
  }
@@ -0,0 +1,142 @@
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 { cleanupLumoStyleSheet, injectLumoStyleSheet } 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
+ * --vaadin-text-field-lumo-inject: 1;
38
+ * --vaadin-text-field-lumo-inject-modules:
39
+ * lumo_base-field,
40
+ * lumo_text-field;
41
+ *
42
+ * --vaadin-email-field-lumo-inject: 1;
43
+ * --vaadin-email-field-lumo-inject-modules:
44
+ * lumo_base-field,
45
+ * lumo_email-field;
46
+ * }
47
+ * ```
48
+ *
49
+ * The class observes the custom property `--{tagName}-lumo-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 `--{tagName}-lumo-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 `--{tagName}-lumo-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
+ constructor(root = document) {
81
+ this.#root = root;
82
+ this.#cssPropertyObserver = new CSSPropertyObserver(this.#root, 'vaadin-lumo-injector', (propertyName) => {
83
+ const tagName = propertyName.slice(2).replace('-lumo-inject', '');
84
+ this.#updateComponentStyleSheet(tagName);
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Adds a component to the list of elements monitored for style injection.
90
+ * If the styles have already been detected, they are injected into the
91
+ * component's shadow DOM immediately. Otherwise, the class watches the
92
+ * custom property `--{tagName}-lumo-inject` to trigger injection when
93
+ * the styles are added to the document or root element.
94
+ *
95
+ * @param {HTMLElement} component
96
+ */
97
+ componentConnected(component) {
98
+ const { is: tagName, lumoInjectPropName } = component.constructor;
99
+
100
+ const stylesheet = this.#styleSheetsByTag.get(tagName) ?? new CSSStyleSheet();
101
+ injectLumoStyleSheet(component, stylesheet);
102
+ this.#styleSheetsByTag.set(tagName, stylesheet);
103
+
104
+ this.#updateComponentStyleSheet(tagName);
105
+
106
+ this.#cssPropertyObserver.observe(lumoInjectPropName);
107
+ }
108
+
109
+ /**
110
+ * Removes the component from the list of elements monitored for
111
+ * style injection and cleans up any previously injected styles.
112
+ *
113
+ * @param {HTMLElement} component
114
+ */
115
+ componentDisconnected(component) {
116
+ cleanupLumoStyleSheet(component);
117
+ }
118
+
119
+ #updateComponentStyleSheet(tagName) {
120
+ const { tags, modules } = parseStyleSheets(this.#rootStyleSheets);
121
+
122
+ const cssText = (tags.get(tagName) ?? [])
123
+ .flatMap((moduleName) => modules.get(moduleName) ?? [])
124
+ .map((rule) => rule.cssText)
125
+ .join('\n');
126
+
127
+ const stylesheet = this.#styleSheetsByTag.get(tagName) ?? new CSSStyleSheet();
128
+ stylesheet.replaceSync(cssText);
129
+ this.#styleSheetsByTag.set(tagName, stylesheet);
130
+ }
131
+
132
+ get #rootStyleSheets() {
133
+ let styleSheets = new Set();
134
+
135
+ for (const root of [this.#root, document]) {
136
+ styleSheets = styleSheets.union(new Set(root.styleSheets));
137
+ styleSheets = styleSheets.union(new Set(root.adoptedStyleSheets));
138
+ }
139
+
140
+ return [...styleSheets];
141
+ }
142
+ }
@@ -0,0 +1,137 @@
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
+ /** @type {WeakMap<CSSStyleSheet, Record<string, Map>>} */
7
+ const cache = new WeakMap();
8
+
9
+ function parseStyleSheet(
10
+ styleSheet,
11
+ result = {
12
+ tags: new Map(),
13
+ modules: new Map(),
14
+ },
15
+ ) {
16
+ let cssRules;
17
+ try {
18
+ cssRules = styleSheet.cssRules;
19
+ } catch {
20
+ // External stylesheets may not be accessible due to CORS security restrictions.
21
+ cssRules = [];
22
+ }
23
+
24
+ for (const rule of cssRules) {
25
+ const { media } = rule;
26
+
27
+ if (rule instanceof CSSImportRule) {
28
+ if (media?.mediaText.startsWith('lumo_')) {
29
+ result.modules.set(media.mediaText, [...rule.styleSheet.cssRules]);
30
+ } else {
31
+ parseStyleSheet(rule.styleSheet, result);
32
+ }
33
+
34
+ continue;
35
+ }
36
+
37
+ if (rule instanceof CSSMediaRule) {
38
+ if (media?.mediaText.startsWith('lumo_')) {
39
+ result.modules.set(media.mediaText, [...rule.cssRules]);
40
+ }
41
+
42
+ continue;
43
+ }
44
+
45
+ if (rule instanceof CSSStyleRule && rule.cssText.includes('-lumo-inject')) {
46
+ for (const property of rule.style) {
47
+ const tagName = property.match(/^--(.*)-lumo-inject-modules$/u)?.[1];
48
+ if (!tagName) {
49
+ continue;
50
+ }
51
+
52
+ const value = rule.style.getPropertyValue(property);
53
+
54
+ result.tags.set(
55
+ tagName,
56
+ value.split(',').map((module) => module.trim().replace(/'|"/gu, '')),
57
+ );
58
+ }
59
+
60
+ continue;
61
+ }
62
+ }
63
+
64
+ return result;
65
+ }
66
+
67
+ /**
68
+ * Parses the provided CSSStyleSheet objects and returns an object with
69
+ * tag-to-modules mappings and module rules.
70
+ *
71
+ * Modules are defined using CSS media rules with names starting with `lumo_`:
72
+ *
73
+ * ```css
74
+ * \@media lumo_base-field {
75
+ * #label {
76
+ * color: gray;
77
+ * }
78
+ * }
79
+ *
80
+ * \@media lumo_text-field {
81
+ * #input {
82
+ * color: yellow;
83
+ * }
84
+ * }
85
+ * ```
86
+ *
87
+ * Also, an entire CSS import can be defined as a module:
88
+ *
89
+ * ```css
90
+ * \@import 'lumo-base-field.css' lumo_base-field;
91
+ * ```
92
+ *
93
+ * Tag-to-modules mappings are defined as CSS custom properties that list
94
+ * the module names to be applied to specific tags, e.g. `vaadin-text-field`:
95
+ *
96
+ * ```css
97
+ * html {
98
+ * --vaadin-text-field-lumo-inject-modules:
99
+ * lumo_base-field,
100
+ * lumo_text-field;
101
+ * }
102
+ * ```
103
+ *
104
+ * Example output:
105
+ *
106
+ * ```js
107
+ * {
108
+ * tags: Map {
109
+ * 'vaadin-text-field': ['lumo_base-field', 'lumo_text-field']
110
+ * },
111
+ * modules: Map {
112
+ * 'lumo_base-field': [CSSStyleRule],
113
+ * 'lumo_text-field': [CSSStyleRule]
114
+ * }
115
+ * }
116
+ * ```
117
+ *
118
+ * @param {CSSStyleSheet[]} styleSheets - An array of CSSStyleSheet objects to parse.
119
+ * @return {{tags: Map<string, string[]>, modules: Map<string, CSSRule[]>}}
120
+ */
121
+ export function parseStyleSheets(styleSheets) {
122
+ let tags = new Map();
123
+ let modules = new Map();
124
+
125
+ for (const styleSheet of styleSheets) {
126
+ let result = cache.get(styleSheet);
127
+ if (!result) {
128
+ result = parseStyleSheet(styleSheet);
129
+ cache.set(styleSheet, result);
130
+ }
131
+
132
+ tags = new Map([...tags, ...result.tags]);
133
+ modules = new Map([...modules, ...result.modules]);
134
+ }
135
+
136
+ return { tags, modules };
137
+ }
@@ -354,11 +354,11 @@ export const ThemableMixin = (superClass) =>
354
354
  */
355
355
  static finalizeStyles(styles) {
356
356
  // 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`.
357
+ // so that they will always be injected before styles added by `LumoInjector`.
358
358
  this.baseStyles = styles ? [styles].flat(Infinity) : [];
359
359
 
360
360
  // Preserve the theme styles the user supplied via the `registerStyles()` API
361
- // so that they will always be injected after styles added by `CSSInjector`.
361
+ // so that they will always be injected after styles added by `LumoInjector`.
362
362
  this.themeStyles = this.getStylesForThis();
363
363
 
364
364
  // Merged styles are stored in `elementStyles` and passed to `adoptStyles()`.
@@ -1,102 +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
- import { CSSPropertyObserver } from './css-property-observer.js';
7
- import { extractTagScopedCSSRules } from './css-rules.js';
8
- import { cleanupStyleSheet, injectStyleSheet } from './css-utils.js';
9
-
10
- /**
11
- * Implements auto-injection of component-scoped CSS styles from document
12
- * style sheets into the Shadow DOM of the corresponding Vaadin components.
13
- *
14
- * Styles are scoped to a component using the following syntax:
15
- *
16
- * 1. `@media vaadin-text-field { ... }` - a media query with a tag name
17
- * 2. `@import "styles.css" vaadin-text-field` - an import rule with a tag name
18
- *
19
- * The class observes the custom property `--{tagName}-css-inject`,
20
- * which indicates the presence of styles for the given component in
21
- * the document style sheets. When the property is set to `1`, the class
22
- * recursively searches all document style sheets for any CSS rules that
23
- * are scoped to the given component tag name using the syntax described
24
- * above. The found rules are then injected into the shadow DOM of all
25
- * subscribed components through the adoptedStyleSheets API.
26
- *
27
- * The class also observes the custom property to remove the styles when
28
- * the property is set to `0`.
29
- *
30
- * If a root element is provided, the class will additionally search for
31
- * component-scoped styles in the root element's style sheets. This is
32
- * useful for embedded Flow applications that are fully isolated from
33
- * the main document and load styles into a component's shadow DOM
34
- * rather than the main document.
35
- *
36
- * WARNING: For internal use only. Do not use this class in custom components.
37
- *
38
- * @private
39
- */
40
- export class CSSInjector {
41
- /** @type {Document | ShadowRoot} */
42
- #root;
43
-
44
- /** @type {CSSPropertyObserver} */
45
- #cssPropertyObserver;
46
-
47
- /** @type {Map<string, CSSStyleSheet>} */
48
- #styleSheetsByTag = new Map();
49
-
50
- constructor(root = document) {
51
- this.#root = root;
52
- this.#cssPropertyObserver = new CSSPropertyObserver(this.#root, 'vaadin-css-injector', (propertyName) => {
53
- const tagName = propertyName.slice(2).replace('-css-inject', '');
54
- this.#updateComponentStyleSheet(tagName);
55
- });
56
- }
57
-
58
- /**
59
- * Adds a component to the list of elements monitored for component-scoped
60
- * styles in global style sheets. If the styles have already been detected,
61
- * they are injected into the component's shadow DOM immediately. Otherwise,
62
- * the class watches the custom property `--{tagName}-css-inject` to trigger
63
- * injection when the styles are added to the document or root element.
64
- *
65
- * @param {HTMLElement} component
66
- */
67
- componentConnected(component) {
68
- const { is: tagName, cssInjectPropName } = component.constructor;
69
-
70
- const stylesheet = this.#styleSheetsByTag.get(tagName) ?? new CSSStyleSheet();
71
- injectStyleSheet(component, stylesheet);
72
- this.#styleSheetsByTag.set(tagName, stylesheet);
73
-
74
- this.#updateComponentStyleSheet(tagName);
75
-
76
- this.#cssPropertyObserver.observe(cssInjectPropName);
77
- }
78
-
79
- /**
80
- * Removes the component from the list of elements monitored for
81
- * component-scoped styles and cleans up any previously injected
82
- * styles from the component's shadow DOM.
83
- *
84
- * @param {HTMLElement} component
85
- */
86
- componentDisconnected(component) {
87
- cleanupStyleSheet(component);
88
- }
89
-
90
- #updateComponentStyleSheet(tagName) {
91
- const roots = new Set([document, this.#root]);
92
-
93
- const cssText = [...roots]
94
- .flatMap((root) => extractTagScopedCSSRules(root, tagName))
95
- .map((rule) => rule.cssText)
96
- .join('\n');
97
-
98
- const stylesheet = this.#styleSheetsByTag.get(tagName) ?? new CSSStyleSheet();
99
- stylesheet.replaceSync(cssText);
100
- this.#styleSheetsByTag.set(tagName, stylesheet);
101
- }
102
- }
package/src/css-rules.js DELETED
@@ -1,133 +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
- const mediaRulesCache = new WeakMap();
9
-
10
- /**
11
- * Check if the media query is a non-standard "tag scoped selector".
12
- *
13
- * Examples of such media queries:
14
- * - `@media vaadin-text-field { ... }`
15
- * - `@import "styles.css" vaadin-text-field`.
16
- *
17
- * @param {string} media
18
- * @return {boolean}
19
- */
20
- function isTagScopedMedia(media) {
21
- return /^\w+(-\w+)+$/u.test(media);
22
- }
23
-
24
- /**
25
- * Recursively processes a style sheet for media rules that match
26
- * the specified predicate.
27
- *
28
- * @param {CSSStyleSheet} styleSheet
29
- * @param {(rule: CSSRule) => boolean} predicate
30
- * @return {Array<CSSMediaRule | CSSImportRule>}
31
- */
32
- function extractMediaRulesFromStyleSheet(styleSheet, predicate) {
33
- const result = [];
34
-
35
- for (const rule of styleSheet.cssRules) {
36
- const ruleType = rule.constructor.name;
37
-
38
- if (ruleType === 'CSSImportRule') {
39
- if (predicate(rule)) {
40
- result.push(rule);
41
- } else {
42
- result.push(...extractMediaRulesFromStyleSheet(rule.styleSheet, predicate));
43
- }
44
- }
45
-
46
- if (ruleType === 'CSSMediaRule') {
47
- if (predicate(rule)) {
48
- result.push(rule);
49
- }
50
- }
51
- }
52
-
53
- return result;
54
- }
55
-
56
- /**
57
- * Deduplicates media rules by their CSS text, keeping the last occurrence.
58
- *
59
- * @param {Array<CSSMediaRule | CSSImportRule>} rules
60
- * @return {Array<CSSMediaRule | CSSImportRule>}
61
- */
62
- function deduplicateMediaRules(rules) {
63
- const seen = new Set();
64
- return rules.reduceRight((deduped, rule) => {
65
- const key = rule.styleSheet?.cssText ?? rule.cssText;
66
- if (!seen.has(key)) {
67
- seen.add(key);
68
- deduped.unshift(rule);
69
- }
70
- return deduped;
71
- }, []);
72
- }
73
-
74
- /**
75
- * Extracts all CSS rules from a style sheet that are contained in media queries
76
- * with a "tag scoped selector" matching the specified tag name.
77
- *
78
- * This function caches the results for each style sheet to avoid
79
- * reprocessing the same style sheet multiple times.
80
- *
81
- * @param {CSSStyleSheet} styleSheet
82
- * @param {string} tagName
83
- * @return {CSSRule[]}
84
- */
85
- function extractTagScopedCSSRulesFromStyleSheet(styleSheet, tagName) {
86
- let mediaRules = mediaRulesCache.get(styleSheet);
87
- if (!mediaRules) {
88
- // Collect all media rules that look like "tag scoped selectors", e.g. "@media vaadin-text-field { ... }"
89
- mediaRules = extractMediaRulesFromStyleSheet(styleSheet, (rule) => isTagScopedMedia(rule.media.mediaText));
90
-
91
- // Remove duplicate media rules which may result from multiple imports of the same stylesheet
92
- mediaRules = deduplicateMediaRules(mediaRules);
93
-
94
- // Group rules by tag name specified in the media query
95
- mediaRules = mediaRules.reduce((acc, rule) => {
96
- const rules = acc.get(rule.media.mediaText) ?? [];
97
- rules.push(rule);
98
- return acc.set(rule.media.mediaText, rules);
99
- }, new Map());
100
-
101
- // Save the processed media rules in the cache
102
- mediaRulesCache.set(styleSheet, mediaRules);
103
- }
104
-
105
- return (mediaRules.get(tagName) ?? []).flatMap((mediaRule) =>
106
- Array.from(mediaRule.styleSheet?.cssRules ?? mediaRule.cssRules),
107
- );
108
- }
109
-
110
- /**
111
- * Recursively processes style sheets of the specified root element, including both
112
- * `adoptedStyleSheets` and regular `styleSheets`, and returns all CSS rules from
113
- * `@media` and `@import` blocks where the media query is (a) "tag scoped selector",
114
- * and (b) matches the specified tag name.
115
- *
116
- * Examples of such media queries:
117
- * - `@media vaadin-text-field { ... }`
118
- * - `@import "styles.css" vaadin-text-field`
119
- *
120
- * The returned rules are ordered in the same way as they are in the original stylesheet.
121
- *
122
- * @param {DocumentOrShadowRoot} root
123
- * @param {string} tagName
124
- * @return {CSSRule[]}
125
- */
126
- export function extractTagScopedCSSRules(root, tagName) {
127
- const styleSheets = new Set(root.styleSheets);
128
- const adoptedStyleSheets = new Set(root.adoptedStyleSheets);
129
-
130
- return [...styleSheets.union(adoptedStyleSheets)].flatMap((styleSheet) => {
131
- return extractTagScopedCSSRulesFromStyleSheet(styleSheet, tagName);
132
- });
133
- }