@vaadin/vaadin-themable-mixin 25.0.0-alpha9 → 25.0.0-beta2

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.
@@ -16,7 +16,7 @@ const registeredProperties = new Set();
16
16
  * @param {HTMLElement} element
17
17
  * @return {DocumentOrShadowRoot}
18
18
  */
19
- function findRoot(element) {
19
+ export function findRoot(element) {
20
20
  const root = element.getRootNode();
21
21
 
22
22
  if (root.host && root.host.constructor.version) {
@@ -58,7 +58,7 @@ export const LumoInjectionMixin = (superClass) =>
58
58
  }
59
59
 
60
60
  static get lumoInjectPropName() {
61
- return `--${this.is}-lumo-inject`;
61
+ return `--_lumo-${this.is}-inject`;
62
62
  }
63
63
 
64
64
  static get lumoInjector() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/vaadin-themable-mixin",
3
- "version": "25.0.0-alpha9",
3
+ "version": "25.0.0-beta2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -33,14 +33,15 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "@open-wc/dedupe-mixin": "^1.3.0",
36
+ "@vaadin/component-base": "25.0.0-beta2",
36
37
  "lit": "^3.0.0"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@polymer/polymer": "^3.0.0",
40
- "@vaadin/chai-plugins": "25.0.0-alpha9",
41
- "@vaadin/test-runner-commands": "25.0.0-alpha9",
41
+ "@vaadin/chai-plugins": "25.0.0-beta2",
42
+ "@vaadin/test-runner-commands": "25.0.0-beta2",
42
43
  "@vaadin/testing-helpers": "^2.0.0",
43
- "sinon": "^18.0.0"
44
+ "sinon": "^21.0.0"
44
45
  },
45
- "gitHead": "bbe4720721e0955ffc87a79b412bee38b1f0eb1e"
46
+ "gitHead": "e078f8371ae266f05c7ca1ec25686cc489c83f24"
46
47
  }
@@ -9,51 +9,52 @@
9
9
  *
10
10
  * @private
11
11
  */
12
- export class CSSPropertyObserver {
12
+ export class CSSPropertyObserver extends EventTarget {
13
13
  #root;
14
- #name;
15
- #callback;
16
14
  #properties = new Set();
17
15
  #styleSheet;
18
16
  #isConnected = false;
19
17
 
20
- constructor(root, name, callback) {
18
+ constructor(root) {
19
+ super();
21
20
  this.#root = root;
22
- this.#name = name;
23
- this.#callback = callback;
21
+ this.#styleSheet = new CSSStyleSheet();
24
22
  }
25
23
 
26
24
  #handleTransitionEvent(event) {
27
25
  const { propertyName } = event;
28
26
  if (this.#properties.has(propertyName)) {
29
- this.#callback(propertyName);
27
+ this.dispatchEvent(new CustomEvent('property-changed', { detail: { propertyName } }));
30
28
  }
31
29
  }
32
30
 
33
31
  observe(property) {
34
32
  this.connect();
35
33
 
36
- this.#properties.add(property);
37
- this.#rootHost.style.setProperty(`--${this.#name}-props`, [...this.#properties].join(', '));
38
- }
39
-
40
- connect() {
41
- if (this.#isConnected) {
34
+ if (this.#properties.has(property)) {
42
35
  return;
43
36
  }
44
37
 
45
- this.#styleSheet = new CSSStyleSheet();
38
+ this.#properties.add(property);
39
+
46
40
  this.#styleSheet.replaceSync(`
47
- :is(:root, :host)::before {
41
+ :root::before, :host::before {
48
42
  content: '' !important;
49
43
  position: absolute !important;
50
44
  top: -9999px !important;
51
45
  left: -9999px !important;
52
46
  visibility: hidden !important;
53
47
  transition: 1ms allow-discrete step-end !important;
54
- transition-property: var(--${this.#name}-props) !important;
48
+ transition-property: ${[...this.#properties].join(', ')} !important;
55
49
  }
56
50
  `);
51
+ }
52
+
53
+ connect() {
54
+ if (this.#isConnected) {
55
+ return;
56
+ }
57
+
57
58
  this.#root.adoptedStyleSheets.unshift(this.#styleSheet);
58
59
 
59
60
  this.#rootHost.addEventListener('transitionstart', (event) => this.#handleTransitionEvent(event));
@@ -69,7 +70,6 @@ export class CSSPropertyObserver {
69
70
 
70
71
  this.#rootHost.removeEventListener('transitionstart', this.#handleTransitionEvent);
71
72
  this.#rootHost.removeEventListener('transitionend', this.#handleTransitionEvent);
72
- this.#rootHost.style.removeProperty(`--${this.#name}-props`);
73
73
 
74
74
  this.#isConnected = false;
75
75
  }
@@ -77,4 +77,14 @@ export class CSSPropertyObserver {
77
77
  get #rootHost() {
78
78
  return this.#root.documentElement ?? this.#root.host;
79
79
  }
80
+
81
+ /**
82
+ * Gets or creates the CSSPropertyObserver for the given root.
83
+ * @param {DocumentOrShadowRoot} root
84
+ * @returns {CSSPropertyObserver}
85
+ */
86
+ static for(root) {
87
+ root.__cssPropertyObserver ||= new CSSPropertyObserver(root);
88
+ return root.__cssPropertyObserver;
89
+ }
80
90
  }
@@ -34,26 +34,26 @@ import { parseStyleSheets } from './lumo-modules.js';
34
34
  * }
35
35
  *
36
36
  * html {
37
- * --vaadin-text-field-lumo-inject: 1;
38
- * --vaadin-text-field-lumo-inject-modules:
37
+ * --_lumo-vaadin-text-field-inject: 1;
38
+ * --_lumo-vaadin-text-field-inject-modules:
39
39
  * lumo_base-field,
40
40
  * lumo_text-field;
41
41
  *
42
- * --vaadin-email-field-lumo-inject: 1;
43
- * --vaadin-email-field-lumo-inject-modules:
42
+ * --_lumo-vaadin-email-field-inject: 1;
43
+ * --_lumo-vaadin-email-field-inject-modules:
44
44
  * lumo_base-field,
45
45
  * lumo_email-field;
46
46
  * }
47
47
  * ```
48
48
  *
49
- * The class observes the custom property `--{tagName}-lumo-inject`,
49
+ * The class observes the custom property `--_lumo-{tagName}-inject`,
50
50
  * which indicates whether styles are present for the given component
51
51
  * in the document style sheets. When the property is set to `1`, the
52
52
  * class recursively searches all document style sheets for CSS modules
53
- * listed in the `--{tagName}-lumo-inject-modules` property that apply to
53
+ * listed in the `--_lumo-{tagName}-inject-modules` property that apply to
54
54
  * the given component tag name. The found rules are then injected
55
55
  * into the component's Shadow DOM using the `adoptedStyleSheets` API,
56
- * in the order specified in the `--{tagName}-lumo-inject-modules` property.
56
+ * in the order specified in the `--_lumo-{tagName}-inject-modules` property.
57
57
  * The same module can be used in multiple components.
58
58
  *
59
59
  * The class also removes the injected styles when the property is set to `0`.
@@ -82,14 +82,13 @@ export class LumoInjector {
82
82
 
83
83
  constructor(root = document) {
84
84
  this.#root = root;
85
- this.#cssPropertyObserver = new CSSPropertyObserver(this.#root, 'vaadin-lumo-injector', (propertyName) => {
86
- const tagName = propertyName.slice(2).replace('-lumo-inject', '');
87
- this.#updateStyleSheet(tagName);
88
- });
85
+ this.handlePropertyChange = this.handlePropertyChange.bind(this);
86
+ this.#cssPropertyObserver = CSSPropertyObserver.for(root);
87
+ this.#cssPropertyObserver.addEventListener('property-changed', this.handlePropertyChange);
89
88
  }
90
89
 
91
90
  disconnect() {
92
- this.#cssPropertyObserver.disconnect();
91
+ this.#cssPropertyObserver.removeEventListener('property-changed', this.handlePropertyChange);
93
92
  this.#styleSheetsByTag.clear();
94
93
  this.#componentsByTag.values().forEach((components) => components.forEach(removeLumoStyleSheet));
95
94
  }
@@ -98,7 +97,7 @@ export class LumoInjector {
98
97
  * Adds a component to the list of elements monitored for style injection.
99
98
  * If the styles have already been detected, they are injected into the
100
99
  * component's shadow DOM immediately. Otherwise, the class watches the
101
- * custom property `--{tagName}-lumo-inject` to trigger injection when
100
+ * custom property `--_lumo-{tagName}-inject` to trigger injection when
102
101
  * the styles are added to the document or root element.
103
102
  *
104
103
  * @param {HTMLElement} component
@@ -134,6 +133,14 @@ export class LumoInjector {
134
133
  removeLumoStyleSheet(component);
135
134
  }
136
135
 
136
+ handlePropertyChange(event) {
137
+ const { propertyName } = event.detail;
138
+ const tagName = propertyName.match(/^--_lumo-(.*)-inject$/u)?.[1];
139
+ if (tagName) {
140
+ this.#updateStyleSheet(tagName);
141
+ }
142
+ }
143
+
137
144
  #initStyleSheet(tagName) {
138
145
  this.#styleSheetsByTag.set(tagName, new CSSStyleSheet());
139
146
  this.#updateStyleSheet(tagName);
@@ -3,9 +3,33 @@
3
3
  * Copyright (c) 2000 - 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 { issueWarning } from '@vaadin/component-base/src/warnings.js';
7
+
6
8
  /** @type {WeakMap<CSSStyleSheet, Record<string, Map>>} */
7
9
  const cache = new WeakMap();
8
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
+
9
33
  function parseStyleSheet(
10
34
  styleSheet,
11
35
  result = {
@@ -13,20 +37,11 @@ function parseStyleSheet(
13
37
  modules: new Map(),
14
38
  },
15
39
  ) {
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
-
40
+ for (const rule of getStyleSheetRules(styleSheet)) {
27
41
  if (rule instanceof CSSImportRule) {
28
- if (media?.mediaText.startsWith('lumo_')) {
29
- result.modules.set(media.mediaText, [...rule.styleSheet.cssRules]);
42
+ const mediaText = getRuleMediaText(rule);
43
+ if (mediaText.startsWith('lumo_')) {
44
+ result.modules.set(mediaText, [...rule.styleSheet.cssRules]);
30
45
  } else {
31
46
  parseStyleSheet(rule.styleSheet, result);
32
47
  }
@@ -35,16 +50,17 @@ function parseStyleSheet(
35
50
  }
36
51
 
37
52
  if (rule instanceof CSSMediaRule) {
38
- if (media?.mediaText.startsWith('lumo_')) {
39
- result.modules.set(media.mediaText, [...rule.cssRules]);
53
+ const mediaText = getRuleMediaText(rule);
54
+ if (mediaText.startsWith('lumo_')) {
55
+ result.modules.set(mediaText, [...rule.cssRules]);
40
56
  }
41
57
 
42
58
  continue;
43
59
  }
44
60
 
45
- if (rule instanceof CSSStyleRule && rule.cssText.includes('-lumo-inject')) {
61
+ if (rule instanceof CSSStyleRule && rule.cssText.includes('-inject')) {
46
62
  for (const property of rule.style) {
47
- const tagName = property.match(/^--(.*)-lumo-inject-modules$/u)?.[1];
63
+ const tagName = property.match(/^--_lumo-(.*)-inject-modules$/u)?.[1];
48
64
  if (!tagName) {
49
65
  continue;
50
66
  }
@@ -95,7 +111,7 @@ function parseStyleSheet(
95
111
  *
96
112
  * ```css
97
113
  * html {
98
- * --vaadin-text-field-lumo-inject-modules:
114
+ * --_lumo-vaadin-text-field-inject-modules:
99
115
  * lumo_base-field,
100
116
  * lumo_text-field;
101
117
  * }
@@ -0,0 +1,78 @@
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 { CSSPropertyObserver } from './css-property-observer.js';
7
+
8
+ // Register CSS custom properties for observing theme changes
9
+ CSS.registerProperty({
10
+ name: '--vaadin-aura-theme',
11
+ syntax: '<number>',
12
+ inherits: true,
13
+ initialValue: '0',
14
+ });
15
+
16
+ CSS.registerProperty({
17
+ name: '--vaadin-lumo-theme',
18
+ syntax: '<number>',
19
+ inherits: true,
20
+ initialValue: '0',
21
+ });
22
+
23
+ /**
24
+ * Observes a root (Document or ShadowRoot) for which Vaadin theme is currently applied.
25
+ * Notifies about theme changes by firing a `theme-changed` event.
26
+ *
27
+ * WARNING: For internal use only. Do not use this class in custom components.
28
+ *
29
+ * @private
30
+ */
31
+ export class ThemeDetector extends EventTarget {
32
+ /** @type {DocumentOrShadowRoot} */
33
+ #root;
34
+ /** @type {CSSPropertyObserver} */
35
+ #observer;
36
+ /** @type {{ aura: boolean; lumo: boolean }} */
37
+ #themes = { aura: false, lumo: false };
38
+ /** @type {(event: CustomEvent) => void} */
39
+ #boundHandleThemeChange = this.#handleThemeChange.bind(this);
40
+
41
+ constructor(root) {
42
+ super();
43
+ this.#root = root;
44
+ this.#detectTheme();
45
+
46
+ this.#observer = CSSPropertyObserver.for(this.#root);
47
+ this.#observer.observe('--vaadin-aura-theme');
48
+ this.#observer.observe('--vaadin-lumo-theme');
49
+ this.#observer.addEventListener('property-changed', this.#boundHandleThemeChange);
50
+ }
51
+
52
+ get themes() {
53
+ return { ...this.#themes };
54
+ }
55
+
56
+ #handleThemeChange(event) {
57
+ const { propertyName } = event.detail;
58
+ if (!['--vaadin-aura-theme', '--vaadin-lumo-theme'].includes(propertyName)) {
59
+ return;
60
+ }
61
+
62
+ this.#detectTheme();
63
+ this.dispatchEvent(new CustomEvent('theme-changed'));
64
+ }
65
+
66
+ #detectTheme() {
67
+ const rootElement = this.#root.documentElement ?? this.#root.host;
68
+ const style = getComputedStyle(rootElement);
69
+ this.#themes = {
70
+ aura: style.getPropertyValue('--vaadin-aura-theme').trim() === '1',
71
+ lumo: style.getPropertyValue('--vaadin-lumo-theme').trim() === '1',
72
+ };
73
+ }
74
+
75
+ disconnect() {
76
+ this.#observer.removeEventListener('property-changed', this.#boundHandleThemeChange);
77
+ }
78
+ }
@@ -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
  /**
@@ -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 };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2025 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import type { Constructor } from '@open-wc/dedupe-mixin';
7
+
8
+ export declare function ThemeDetectionMixin<T extends Constructor<HTMLElement>>(
9
+ base: T,
10
+ ): Constructor<ThemeDetectionMixinClass> & T;
11
+
12
+ export declare class ThemeDetectionMixinClass {}
@@ -0,0 +1,58 @@
1
+ import { findRoot } from './lumo-injection-mixin.js';
2
+ import { ThemeDetector } from './src/theme-detector.js';
3
+
4
+ /**
5
+ * Mixin for detecting which Vaadin theme is applied to the application.
6
+ * Automatically adds a `data-application-theme` attribute to the host
7
+ * element with the name of the detected theme (`lumo` or `aura`), which
8
+ * can be used in component styles to apply theme-specific styling.
9
+ *
10
+ * @polymerMixin
11
+ */
12
+ export const ThemeDetectionMixin = (superClass) =>
13
+ class ThemeDetectionMixinClass extends superClass {
14
+ constructor() {
15
+ super();
16
+
17
+ this.__applyDetectedTheme = this.__applyDetectedTheme.bind(this);
18
+ }
19
+
20
+ /** @protected */
21
+ connectedCallback() {
22
+ super.connectedCallback();
23
+
24
+ if (this.isConnected) {
25
+ const root = findRoot(this);
26
+ root.__themeDetector = root.__themeDetector || new ThemeDetector(root);
27
+ this.__themeDetector = root.__themeDetector;
28
+ this.__themeDetector.addEventListener('theme-changed', this.__applyDetectedTheme);
29
+ this.__applyDetectedTheme();
30
+ }
31
+ }
32
+
33
+ /** @protected */
34
+ disconnectedCallback() {
35
+ super.disconnectedCallback();
36
+
37
+ if (this.__themeDetector) {
38
+ this.__themeDetector.removeEventListener('theme-changed', this.__applyDetectedTheme);
39
+ this.__themeDetector = null;
40
+ }
41
+ }
42
+
43
+ /** @private */
44
+ __applyDetectedTheme() {
45
+ if (!this.__themeDetector) {
46
+ return;
47
+ }
48
+
49
+ const themes = this.__themeDetector.themes;
50
+ if (themes.aura) {
51
+ this.dataset.applicationTheme = 'aura';
52
+ } else if (themes.lumo) {
53
+ this.dataset.applicationTheme = 'lumo';
54
+ } else {
55
+ delete this.dataset.applicationTheme;
56
+ }
57
+ }
58
+ };