Skip to content
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

Open
wants to merge 1 commit into
base: next
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 56 additions & 65 deletions packages/controllers/adopted-stylesheets/src/adopted-stylesheets.ts
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();
Comment on lines +14 to +15
Copy link

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.


/**
* 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method applyCssToDom is well-implemented, with clear separation of logic for applying styles to the Document and ShadowRoot. However, using CSSStyleSheet and adoptedStyleSheets requires the Shadow DOM to be enabled and might not be supported in all browsers. Ensure that there's a fallback mechanism or a polyfill for browsers that do not support these features.

}

/**
* 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hostConnected lifecycle callback efficiently applies global and encapsulated CSS to the appropriate DOM targets. This implementation aligns with the PR's objective to improve the management of CSS styles. Ensure that the CSS strings are sanitized or validated to prevent potential security issues like CSS injection.

}
}

/**
* 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.
}
}
Loading