@vaadin/vaadin-themable-mixin 24.4.0-dev.b3e1d14600 → 24.5.0-alpha1

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.
package/README.md CHANGED
@@ -5,7 +5,6 @@ A mixin to enable customization of Shadow DOM used by Vaadin components.
5
5
  [Documentation ↗](https://vaadin.com/docs/latest/styling/styling-components)
6
6
 
7
7
  [![npm version](https://badgen.net/npm/v/@vaadin/vaadin-themable-mixin)](https://www.npmjs.com/package/@vaadin/vaadin-themable-mixin)
8
- [![Discord](https://img.shields.io/discord/732335336448852018?label=discord)](https://discord.gg/PHmkCKC)
9
8
 
10
9
  ## License
11
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/vaadin-themable-mixin",
3
- "version": "24.4.0-dev.b3e1d14600",
3
+ "version": "24.5.0-alpha1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -40,5 +40,5 @@
40
40
  "@vaadin/testing-helpers": "^0.6.0",
41
41
  "sinon": "^13.0.2"
42
42
  },
43
- "gitHead": "502d4f5b03f770a83d270d98078cde230254dd0e"
43
+ "gitHead": "57806caac5468532a3b4e3dbdda730cd0fca193a"
44
44
  }
@@ -2,7 +2,7 @@ import type { CSSResultGroup } from 'lit';
2
2
 
3
3
  /**
4
4
  * @license
5
- * Copyright (c) 2017 - 2023 Vaadin Ltd.
5
+ * Copyright (c) 2017 - 2024 Vaadin Ltd.
6
6
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
7
7
  */
8
8
  export { registerStyles, css, unsafeCSS } from './vaadin-themable-mixin.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2017 - 2023 Vaadin Ltd.
3
+ * Copyright (c) 2017 - 2024 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  export { registerStyles, css, unsafeCSS } from './vaadin-themable-mixin.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2017 - 2023 Vaadin Ltd.
3
+ * Copyright (c) 2017 - 2024 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import type { Constructor } from '@open-wc/dedupe-mixin';
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2017 - 2023 Vaadin Ltd.
3
+ * Copyright (c) 2017 - 2024 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import { css, CSSResult, unsafeCSS } from 'lit';
6
+ import { adoptStyles, css, CSSResult, LitElement, unsafeCSS } from 'lit';
7
7
  import { ThemePropertyMixin } from './vaadin-theme-property-mixin.js';
8
8
 
9
9
  export { css, unsafeCSS };
@@ -23,6 +23,16 @@ export { css, unsafeCSS };
23
23
  */
24
24
  const themeRegistry = [];
25
25
 
26
+ /**
27
+ * @type {WeakRef<HTMLElement>[]}
28
+ */
29
+ const themableInstances = new Set();
30
+
31
+ /**
32
+ * @type {string[]}
33
+ */
34
+ const themableTagNames = new Set();
35
+
26
36
  /**
27
37
  * Check if the custom element type has themes applied.
28
38
  * @param {Function} elementClass
@@ -57,6 +67,129 @@ function flattenStyles(styles = []) {
57
67
  });
58
68
  }
59
69
 
70
+ /**
71
+ * Returns true if the themeFor string matches the tag name
72
+ * @param {string} themeFor
73
+ * @param {string} tagName
74
+ * @returns {boolean}
75
+ */
76
+ function matchesThemeFor(themeFor, tagName) {
77
+ return (themeFor || '').split(' ').some((themeForToken) => {
78
+ return new RegExp(`^${themeForToken.split('*').join('.*')}$`, 'u').test(tagName);
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Returns the CSS text content from an array of CSSResults
84
+ * @param {CSSResult[]} styles
85
+ * @returns {string}
86
+ */
87
+ function getCssText(styles) {
88
+ return styles.map((style) => style.cssText).join('\n');
89
+ }
90
+
91
+ const STYLE_ID = 'vaadin-themable-mixin-style';
92
+
93
+ /**
94
+ * Includes the styles to the template.
95
+ * @param {CSSResult[]} styles
96
+ * @param {HTMLTemplateElement} template
97
+ */
98
+ function addStylesToTemplate(styles, template) {
99
+ const styleEl = document.createElement('style');
100
+ styleEl.id = STYLE_ID;
101
+ styleEl.textContent = getCssText(styles);
102
+ template.content.appendChild(styleEl);
103
+ }
104
+
105
+ /**
106
+ * Dynamically updates the styles of the given component instance.
107
+ * @param {HTMLElement} instance
108
+ */
109
+ function updateInstanceStyles(instance) {
110
+ if (!instance.shadowRoot) {
111
+ return;
112
+ }
113
+
114
+ const componentClass = instance.constructor;
115
+
116
+ if (instance instanceof LitElement) {
117
+ // LitElement
118
+
119
+ // The adoptStyles function may fall back to appending style elements to shadow root.
120
+ // Remove them first to avoid duplicates.
121
+ [...instance.shadowRoot.querySelectorAll('style')].forEach((style) => style.remove());
122
+
123
+ // Adopt the updated styles
124
+ adoptStyles(instance.shadowRoot, componentClass.elementStyles);
125
+ } else {
126
+ // PolymerElement
127
+
128
+ // Update style element content in the shadow root
129
+ const style = instance.shadowRoot.getElementById(STYLE_ID);
130
+ const template = componentClass.prototype._template;
131
+ style.textContent = template.content.getElementById(STYLE_ID).textContent;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Dynamically updates the styles of the instances matching the given component type.
137
+ * @param {Function} componentClass
138
+ */
139
+ function updateInstanceStylesOfType(componentClass) {
140
+ // Iterate over component instances and update their styles if needed
141
+ themableInstances.forEach((ref) => {
142
+ const instance = ref.deref();
143
+ if (instance instanceof componentClass) {
144
+ updateInstanceStyles(instance);
145
+ } else if (!instance) {
146
+ // Clean up the weak reference to a GC'd instance
147
+ themableInstances.delete(ref);
148
+ }
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Dynamically updates the styles of the given component type.
154
+ * @param {Function} componentClass
155
+ */
156
+ function updateComponentStyles(componentClass) {
157
+ if (componentClass.prototype instanceof LitElement) {
158
+ // Update LitElement-based component's elementStyles
159
+ componentClass.elementStyles = componentClass.finalizeStyles(componentClass.styles);
160
+ } else {
161
+ // Update Polymer-based component's template
162
+ const template = componentClass.prototype._template;
163
+ template.content.getElementById(STYLE_ID).textContent = getCssText(componentClass.getStylesForThis());
164
+ }
165
+
166
+ // Update the styles of inheriting types
167
+ themableTagNames.forEach((inheritingTagName) => {
168
+ const inheritingClass = customElements.get(inheritingTagName);
169
+ if (inheritingClass !== componentClass && inheritingClass.prototype instanceof componentClass) {
170
+ updateComponentStyles(inheritingClass);
171
+ }
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Check if the component type already has a style matching the given styles.
177
+ *
178
+ * @param {Function} componentClass
179
+ * @param {CSSResultGroup} styles
180
+ * @returns {boolean}
181
+ */
182
+ function hasMatchingStyle(componentClass, styles) {
183
+ const themes = componentClass.__themes;
184
+ if (!themes || !styles) {
185
+ return false;
186
+ }
187
+
188
+ return themes.some((theme) =>
189
+ theme.styles.some((themeStyle) => styles.some((style) => style.cssText === themeStyle.cssText)),
190
+ );
191
+ }
192
+
60
193
  /**
61
194
  * Registers CSS styles for a component type. Make sure to register the styles before
62
195
  * the first instance of a component of the type is attached to DOM.
@@ -68,15 +201,6 @@ function flattenStyles(styles = []) {
68
201
  * @return {void}
69
202
  */
70
203
  export function registerStyles(themeFor, styles, options = {}) {
71
- if (themeFor) {
72
- if (hasThemes(themeFor)) {
73
- console.warn(`The custom element definition for "${themeFor}"
74
- was finalized before a style module was registered.
75
- Make sure to add component specific style modules before
76
- importing the corresponding custom element.`);
77
- }
78
- }
79
-
80
204
  styles = flattenStyles(styles);
81
205
 
82
206
  if (window.Vaadin && window.Vaadin.styleModules) {
@@ -89,6 +213,34 @@ export function registerStyles(themeFor, styles, options = {}) {
89
213
  moduleId: options.moduleId,
90
214
  });
91
215
  }
216
+
217
+ if (themeFor) {
218
+ // Update styles of the component types that match themeFor and have already been finalized
219
+ themableTagNames.forEach((tagName) => {
220
+ if (matchesThemeFor(themeFor, tagName) && hasThemes(tagName)) {
221
+ const componentClass = customElements.get(tagName);
222
+
223
+ if (hasMatchingStyle(componentClass, styles)) {
224
+ // Show a warning if the component type already has some of the given styles
225
+ console.warn(`Registering styles that already exist for ${tagName}`);
226
+ } else if (!window.Vaadin || !window.Vaadin.suppressPostFinalizeStylesWarning) {
227
+ // Show a warning if the component type has already been finalized
228
+ console.warn(
229
+ `The custom element definition for "${tagName}" ` +
230
+ `was finalized before a style module was registered. ` +
231
+ `Ideally, import component specific style modules before ` +
232
+ `importing the corresponding custom element. ` +
233
+ `This warning can be suppressed by setting "window.Vaadin.suppressPostFinalizeStylesWarning = true".`,
234
+ );
235
+ }
236
+
237
+ // Update the styles of the component type
238
+ updateComponentStyles(componentClass);
239
+ // Update the styles of the component instances matching the component type
240
+ updateInstanceStylesOfType(componentClass);
241
+ }
242
+ });
243
+ }
92
244
  }
93
245
 
94
246
  /**
@@ -103,18 +255,6 @@ function getAllThemes() {
103
255
  return themeRegistry;
104
256
  }
105
257
 
106
- /**
107
- * Returns true if the themeFor string matches the tag name
108
- * @param {string} themeFor
109
- * @param {string} tagName
110
- * @returns {boolean}
111
- */
112
- function matchesThemeFor(themeFor, tagName) {
113
- return (themeFor || '').split(' ').some((themeForToken) => {
114
- return new RegExp(`^${themeForToken.split('*').join('.*')}$`, 'u').test(tagName);
115
- });
116
- }
117
-
118
258
  /**
119
259
  * Maps the moduleName to an include priority number which is used for
120
260
  * determining the order in which styles are applied.
@@ -151,17 +291,6 @@ function getIncludedStyles(theme) {
151
291
  return includedStyles;
152
292
  }
153
293
 
154
- /**
155
- * Includes the styles to the template.
156
- * @param {CSSResult[]} styles
157
- * @param {HTMLTemplateElement} template
158
- */
159
- function addStylesToTemplate(styles, template) {
160
- const styleEl = document.createElement('style');
161
- styleEl.innerHTML = styles.map((style) => style.cssText).join('\n');
162
- template.content.appendChild(styleEl);
163
- }
164
-
165
294
  /**
166
295
  * Returns an array of themes that should be used for styling a component matching
167
296
  * the tag name. The array is sorted by the include order.
@@ -197,6 +326,12 @@ function getThemes(tagName) {
197
326
  */
198
327
  export const ThemableMixin = (superClass) =>
199
328
  class VaadinThemableMixin extends ThemePropertyMixin(superClass) {
329
+ constructor() {
330
+ super();
331
+ // Store a weak reference to the instance
332
+ themableInstances.add(new WeakRef(this));
333
+ }
334
+
200
335
  /**
201
336
  * Covers PolymerElement based component styling
202
337
  * @protected
@@ -204,6 +339,10 @@ export const ThemableMixin = (superClass) =>
204
339
  static finalize() {
205
340
  super.finalize();
206
341
 
342
+ if (this.is) {
343
+ themableTagNames.add(this.is);
344
+ }
345
+
207
346
  // Make sure not to run the logic intended for PolymerElement when LitElement is used.
208
347
  if (this.elementStyles) {
209
348
  return;
@@ -227,7 +366,7 @@ export const ThemableMixin = (superClass) =>
227
366
  // a LitElement based component. The theme styles are added after it
228
367
  // so that they can override the component styles.
229
368
  const themeStyles = this.getStylesForThis();
230
- return styles ? [...super.finalizeStyles(styles), ...themeStyles] : themeStyles;
369
+ return styles ? [...[styles].flat(Infinity), ...themeStyles] : themeStyles;
231
370
  }
232
371
 
233
372
  /**
@@ -236,9 +375,10 @@ export const ThemableMixin = (superClass) =>
236
375
  * @private
237
376
  */
238
377
  static getStylesForThis() {
378
+ const superClassThemes = superClass.__themes || [];
239
379
  const parent = Object.getPrototypeOf(this.prototype);
240
380
  const inheritedThemes = (parent ? parent.constructor.__themes : []) || [];
241
- this.__themes = [...inheritedThemes, ...getThemes(this.is)];
381
+ this.__themes = [...superClassThemes, ...inheritedThemes, ...getThemes(this.is)];
242
382
  const themeStyles = this.__themes.flatMap((theme) => theme.styles);
243
383
  // Remove duplicates
244
384
  return themeStyles.filter((style, index) => index === themeStyles.lastIndexOf(style));
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2017 - 2023 Vaadin Ltd.
3
+ * Copyright (c) 2017 - 2024 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import type { Constructor } from '@open-wc/dedupe-mixin';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @license
3
- * Copyright (c) 2017 - 2023 Vaadin Ltd.
3
+ * Copyright (c) 2017 - 2024 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  /**