@vaadin/vaadin-themable-mixin 25.0.0-beta1 → 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) {
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-beta2",
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-beta2",
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-beta2",
42
+ "@vaadin/test-runner-commands": "25.0.0-beta2",
43
43
  "@vaadin/testing-helpers": "^2.0.0",
44
44
  "sinon": "^21.0.0"
45
45
  },
46
- "gitHead": "1d20cf54e582d1f2e209126d4586f8b4c01c50e0"
46
+ "gitHead": "e078f8371ae266f05c7ca1ec25686cc489c83f24"
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
  }
@@ -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, (propertyName) => {
86
- const tagName = propertyName.match(/^--_lumo-(.*)-inject$/u)?.[1];
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
  }
@@ -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);
@@ -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
+ };