@vaadin/vaadin-themable-mixin 25.0.0-beta1 → 25.0.0-beta3

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 { LumoInjector } from './src/lumo-injector.js';
6
+ import { getLumoInjectorPropName, LumoInjector } from './src/lumo-injector.js';
7
7
 
8
8
  /**
9
9
  * @type {Set<string>}
@@ -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) {
@@ -36,7 +36,7 @@ export const LumoInjectionMixin = (superClass) =>
36
36
  static finalize() {
37
37
  super.finalize();
38
38
 
39
- const propName = this.lumoInjectPropName;
39
+ const propName = getLumoInjectorPropName(this.lumoInjector);
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,12 +57,9 @@ export const LumoInjectionMixin = (superClass) =>
57
57
  }
58
58
  }
59
59
 
60
- static get lumoInjectPropName() {
61
- return `--_lumo-${this.is}-inject`;
62
- }
63
-
64
60
  static get lumoInjector() {
65
61
  return {
62
+ is: this.is,
66
63
  includeBaseStyles: false,
67
64
  };
68
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/vaadin-themable-mixin",
3
- "version": "25.0.0-beta1",
3
+ "version": "25.0.0-beta3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -33,15 +33,15 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "@open-wc/dedupe-mixin": "^1.3.0",
36
- "@vaadin/component-base": "25.0.0-beta1",
36
+ "@vaadin/component-base": "25.0.0-beta3",
37
37
  "lit": "^3.0.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@polymer/polymer": "^3.0.0",
41
- "@vaadin/chai-plugins": "25.0.0-beta1",
42
- "@vaadin/test-runner-commands": "25.0.0-beta1",
41
+ "@vaadin/chai-plugins": "25.0.0-beta3",
42
+ "@vaadin/test-runner-commands": "25.0.0-beta3",
43
43
  "@vaadin/testing-helpers": "^2.0.0",
44
44
  "sinon": "^21.0.0"
45
45
  },
46
- "gitHead": "1d20cf54e582d1f2e209126d4586f8b4c01c50e0"
46
+ "gitHead": "4b2006b0e2f4fc131f5483223b852d34224e7b9a"
47
47
  }
@@ -9,23 +9,22 @@
9
9
  *
10
10
  * @private
11
11
  */
12
- export class CSSPropertyObserver {
12
+ export class CSSPropertyObserver extends EventTarget {
13
13
  #root;
14
- #callback;
15
14
  #properties = new Set();
16
15
  #styleSheet;
17
16
  #isConnected = false;
18
17
 
19
- constructor(root, callback) {
18
+ constructor(root) {
19
+ super();
20
20
  this.#root = root;
21
- this.#callback = callback;
22
21
  this.#styleSheet = new CSSStyleSheet();
23
22
  }
24
23
 
25
24
  #handleTransitionEvent(event) {
26
25
  const { propertyName } = event;
27
26
  if (this.#properties.has(propertyName)) {
28
- this.#callback(propertyName);
27
+ this.dispatchEvent(new CustomEvent('property-changed', { detail: { propertyName } }));
29
28
  }
30
29
  }
31
30
 
@@ -78,4 +77,14 @@ export class CSSPropertyObserver {
78
77
  get #rootHost() {
79
78
  return this.#root.documentElement ?? this.#root.host;
80
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
+ }
81
90
  }
@@ -7,6 +7,10 @@ import { CSSPropertyObserver } from './css-property-observer.js';
7
7
  import { injectLumoStyleSheet, removeLumoStyleSheet } from './css-utils.js';
8
8
  import { parseStyleSheets } from './lumo-modules.js';
9
9
 
10
+ export function getLumoInjectorPropName(lumoInjector) {
11
+ return `--_lumo-${lumoInjector.is}-inject`;
12
+ }
13
+
10
14
  /**
11
15
  * Implements auto-injection of CSS styles from document style sheets
12
16
  * into the Shadow DOM of corresponding Vaadin components.
@@ -82,14 +86,13 @@ export class LumoInjector {
82
86
 
83
87
  constructor(root = document) {
84
88
  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
+ this.handlePropertyChange = this.handlePropertyChange.bind(this);
90
+ this.#cssPropertyObserver = CSSPropertyObserver.for(root);
91
+ this.#cssPropertyObserver.addEventListener('property-changed', this.handlePropertyChange);
89
92
  }
90
93
 
91
94
  disconnect() {
92
- this.#cssPropertyObserver.disconnect();
95
+ this.#cssPropertyObserver.removeEventListener('property-changed', this.handlePropertyChange);
93
96
  this.#styleSheetsByTag.clear();
94
97
  this.#componentsByTag.values().forEach((components) => components.forEach(removeLumoStyleSheet));
95
98
  }
@@ -104,7 +107,8 @@ export class LumoInjector {
104
107
  * @param {HTMLElement} component
105
108
  */
106
109
  componentConnected(component) {
107
- const { is: tagName, lumoInjectPropName } = component.constructor;
110
+ const { lumoInjector } = component.constructor;
111
+ const { is: tagName } = lumoInjector;
108
112
 
109
113
  this.#componentsByTag.set(tagName, this.#componentsByTag.get(tagName) ?? new Set());
110
114
  this.#componentsByTag.get(tagName).add(component);
@@ -118,7 +122,9 @@ export class LumoInjector {
118
122
  }
119
123
 
120
124
  this.#initStyleSheet(tagName);
121
- this.#cssPropertyObserver.observe(lumoInjectPropName);
125
+
126
+ const propName = getLumoInjectorPropName(lumoInjector);
127
+ this.#cssPropertyObserver.observe(propName);
122
128
  }
123
129
 
124
130
  /**
@@ -128,12 +134,20 @@ export class LumoInjector {
128
134
  * @param {HTMLElement} component
129
135
  */
130
136
  componentDisconnected(component) {
131
- const { is: tagName } = component.constructor;
137
+ const { is: tagName } = component.constructor.lumoInjector;
132
138
  this.#componentsByTag.get(tagName)?.delete(component);
133
139
 
134
140
  removeLumoStyleSheet(component);
135
141
  }
136
142
 
143
+ handlePropertyChange(event) {
144
+ const { propertyName } = event.detail;
145
+ const tagName = propertyName.match(/^--_lumo-(.*)-inject$/u)?.[1];
146
+ if (tagName) {
147
+ this.#updateStyleSheet(tagName);
148
+ }
149
+ }
150
+
137
151
  #initStyleSheet(tagName) {
138
152
  this.#styleSheetsByTag.set(tagName, new CSSStyleSheet());
139
153
  this.#updateStyleSheet(tagName);
@@ -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
+ }
@@ -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
+ };