-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: simplify adopted stylesheets controller #441
base: next
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,91 +1,82 @@ | ||
import { ReactiveController, ReactiveControllerHost } from 'lit'; | ||
|
||
/** | ||
* `AdoptedStylesheets` is a class that implements the `ReactiveController` interface from the `lit` library. | ||
* This class is used to manage CSS stylesheets that are adopted into the document or a shadow root. | ||
* | ||
* @property {CSSStyleSheet} adoptedSheet - The CSSStyleSheet object that is adopted into the document or a shadow root. | ||
* @property {Document | ShadowRoot} root - The root where the stylesheet will be adopted. | ||
* A controller for managing adopted stylesheets in a Lit element. | ||
* This allows for styles to be dynamically applied to the component's | ||
* light DOM or shadow DOM. | ||
*/ | ||
export class AdoptedStylesheets implements ReactiveController { | ||
/** | ||
* A static map that stores CSSStyleSheet objects by their CSS text. | ||
* This allows for reuse of CSSStyleSheet objects across multiple instances of the class. | ||
* @type {Map<string, CSSStyleSheet>} | ||
*/ | ||
private static styleSheetMap = new Map<string, CSSStyleSheet>(); | ||
|
||
/** | ||
* The CSSStyleSheet object that is adopted into the document or a shadow root. | ||
* @type {CSSStyleSheet} | ||
*/ | ||
private adoptedSheet: CSSStyleSheet; | ||
export class AdoptedStyleSheets implements ReactiveController { | ||
// The host element that the controller is associated with. | ||
private host: ReactiveControllerHost & HTMLElement; | ||
// An object containing the CSS to be applied globally or encapsulated within the shadow DOM. | ||
private css: { globalCSS?: string; encapsulatedCSS?: string }; | ||
|
||
/** | ||
* The root where the stylesheet will be adopted. | ||
* This can be either the document or a shadow root. | ||
* @type {Document | ShadowRoot} | ||
*/ | ||
private root: Document | ShadowRoot; | ||
|
||
/** | ||
* The host that this controller is associated with. | ||
* @type {ReactiveControllerHost} | ||
*/ | ||
private host: ReactiveControllerHost; | ||
// A set to track the stylesheets applied to the light DOM. | ||
private static appliedLightDomStylesheets: Set<string> = new Set(); | ||
|
||
/** | ||
* The constructor for the `AdoptedStylesheets` class. | ||
* | ||
* @param {ReactiveControllerHost} host - The host that this controller is associated with. | ||
* @param {string} cssText - A string that contains the CSS styles to be adopted. | ||
* @param {Document | ShadowRoot} root - The root where the stylesheet will be adopted. | ||
* Constructs an instance of the AdoptedStyleSheets controller. | ||
* @param host The host element that the controller will be associated with. | ||
* @param css An object containing optional global and encapsulated CSS strings. | ||
*/ | ||
constructor( | ||
host: ReactiveControllerHost, | ||
cssText: string, | ||
root: Document | ShadowRoot = document | ||
host: ReactiveControllerHost & HTMLElement, | ||
css: { | ||
globalCSS?: string; | ||
encapsulatedCSS?: string; | ||
} = {} | ||
) { | ||
this.host = host; | ||
this.host.addController(this); | ||
this.root = root; | ||
this.css = css; | ||
this.host.addController(this); // Register this instance as a controller for the host element. | ||
} | ||
|
||
if (!AdoptedStylesheets.styleSheetMap.has(cssText)) { | ||
const newSheet = new CSSStyleSheet(); | ||
newSheet.replace(cssText).catch(error => { | ||
console.error('Failed to replace CSS text:', error); | ||
}); | ||
AdoptedStylesheets.styleSheetMap.set(cssText, newSheet); | ||
/** | ||
* Applies the given CSS text to the specified target (Document or ShadowRoot). | ||
* @param cssText The CSS text to apply. | ||
* @param target The target where the CSS should be applied. | ||
*/ | ||
private applyCssToDom(cssText: string, target: Document | ShadowRoot) { | ||
if (target instanceof Document) { | ||
const store = AdoptedStyleSheets.appliedLightDomStylesheets; | ||
|
||
if (store.has(cssText)) { | ||
// If the stylesheet has already been applied, no further action is required. | ||
return; | ||
} | ||
store.add(cssText); // Store the stylesheet with the provided key. | ||
} | ||
this.adoptedSheet = | ||
AdoptedStylesheets.styleSheetMap.get(cssText) || new CSSStyleSheet(); | ||
|
||
// Create a new stylesheet and replace its contents with the provided CSS text. | ||
const sheet = new CSSStyleSheet(); | ||
sheet.replaceSync(cssText); | ||
|
||
// Apply the stylesheet to the target's adoptedStyleSheets. | ||
target.adoptedStyleSheets = [...target.adoptedStyleSheets, sheet]; | ||
Comment on lines
+39
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The method |
||
} | ||
|
||
/** | ||
* The `hostConnected` method is called when the host element is connected to the DOM. | ||
* This method adopts the CSSStyleSheet object into the root's adopted stylesheets if it's not already included. | ||
* Lifecycle callback called when the host element is connected to the document's DOM. | ||
* Applies global and encapsulated CSS to the respective DOM targets. | ||
*/ | ||
hostConnected() { | ||
if ( | ||
this.root && | ||
!this.root.adoptedStyleSheets.includes(this.adoptedSheet) | ||
) { | ||
this.root.adoptedStyleSheets = [ | ||
...this.root.adoptedStyleSheets, | ||
this.adoptedSheet, | ||
]; | ||
if (this.css.globalCSS) { | ||
this.applyCssToDom(this.css.globalCSS, document); // Apply global CSS to the document if it exists. | ||
} | ||
|
||
// Apply encapsulated CSS to the host's shadow root if it exists. | ||
if (this.css.encapsulatedCSS && this.host.shadowRoot) { | ||
this.applyCssToDom(this.css.encapsulatedCSS, this.host.shadowRoot); // Apply encapsulated CSS to the host's shadow root if it exists. | ||
Comment on lines
+63
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
} | ||
} | ||
|
||
/** | ||
* The `hostDisconnected` method is called when the host element is disconnected from the DOM. | ||
* This method removes the CSSStyleSheet object from the root's adopted stylesheets if it's included. | ||
* Lifecycle callback called when the host element is disconnected from the document's DOM. | ||
* Note: When a component with a Shadow DOM is disconnected from the document's DOM, the Shadow DOM is also removed along with the component. | ||
* However, for Light DOM styles, they are not removed here because other instances of the component | ||
* might still be present on the page and require these styles. | ||
*/ | ||
hostDisconnected() { | ||
if (this.root && this.root.adoptedStyleSheets.includes(this.adoptedSheet)) { | ||
this.root.adoptedStyleSheets = this.root.adoptedStyleSheets.filter( | ||
sheet => sheet !== this.adoptedSheet | ||
); | ||
} | ||
// No action is taken when the host is disconnected. | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a static property
appliedLightDomStylesheets
to track stylesheets applied to the light DOM is a good approach to prevent duplicate styles from being applied. However, ensure that this does not lead to memory leaks by holding references to styles that are no longer needed. Consider implementing a cleanup mechanism if necessary.