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

AdoptedStyleSheets Update: Attach styles to document or this.shadowRoot #431

Merged
merged 10 commits into from
Feb 16, 2024
70 changes: 59 additions & 11 deletions packages/controllers/adopted-stylesheets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,26 @@

[![Latest version for outline-adopted-stylesheets-controller](https://img.shields.io/npm/v/@phase2/outline-adopted-stylesheets-controller)](https://www.npmjs.com/package/@phase2/outline-adopted-stylesheets-controller)

> The `AdoptedStylesheets` controller is a part of the `@phase2/outline-adopted-stylesheets-controller` package. This controller helps components attach "global" document styles without duplication at the component level as well as de-duping any previous inclusions into `AdoptedStylesheets`.
> The `AdoptedStylesheets` controller is a part of the `@phase2/outline-adopted-stylesheets-controller` package. This controller assists components in attaching "global" document styles without duplication at the component level, as well as de-duping any previous inclusions into `AdoptedStylesheets`.

## Installing the `AdoptedStylesheets` Controller

Install the new package:
To install the new package, use the following command, specific to your package manager:

```bash
# With Yarn
yarn add @phase2/outline-adopted-stylesheets-controller

# With NPM
npm i --save-dev @phase2/outline-adopted-stylesheets-controller

# With PNPM
pnpm add --save-dev @phase2/outline-adopted-stylesheets-controller
```

## Overview

Adopted stylesheets are a method to apply styles to a document or a shadow root. They are a part of the CSS Shadow Parts specification. Unfortunately, the original documentation link is no longer available. However, you can find more information about adopted stylesheets and their usage in web components in the following resources:
Adopted stylesheets are a method to apply styles to a document or a shadow root. They are a part of the CSS Shadow Parts specification. For more information about adopted stylesheets and their usage in web components, refer to the following resources:

- [MDN Web Docs: Document adoptedStyleSheets](https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets): This documentation provides an in-depth look at the `adoptedStyleSheets` property of the `Document` interface.
- [MDN Web Docs: ShadowRoot adoptedStyleSheets](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/adoptedStyleSheets): This documentation provides an in-depth look at the `adoptedStyleSheets` property of the `ShadowRoot` interface.
Expand All @@ -23,31 +30,72 @@ Adopted stylesheets are a method to apply styles to a document or a shadow root.

The `AdoptedStylesheets` controller provides the following methods:

- `constructor(globalStyles: CSSResult)`: This method is used to create a new instance of the `AdoptedStylesheets` controller. It takes a `CSSResult` object as a parameter, which represents the global styles to be adopted.
- `constructor(globalStyles: CSSResult, root: Document | ShadowRoot)`: This method is used to create a new instance of the `AdoptedStylesheets` controller. It takes a `CSSResult` object and a root (either a `Document` or `ShadowRoot`) as parameters.

- `hostConnected()`: This method is called when the host element is connected to the DOM. It adds the document's stylesheet to the adopted stylesheets if it is not already present.

- `hostDisconnected()`: This method is called when the host element is disconnected from the DOM. It removes the document's stylesheet from the adopted stylesheets if it is present.

## Usage

Here is an example of how to use the `AdoptedStylesheets` controller in a component:
Here are examples of how to use the `AdoptedStylesheets` controller in a component:

### Sample 1: Attach a stylesheet to `document`

```typescript
import { AdoptedStylesheets } from '@phase2/outline-adopted-stylesheets-controller';
import { css, CSSResult } from 'lit';
import { OutlineElement } from '@phase2/outline-core';
import globalStyles from './my-component.lightDom.css.lit';
import { css, CSSResult, LitElement } from 'lit';
import globalStyles from './global-styles';

class MyComponent extends OutlineElement {
class MyComponent extends LitElement {
AdoptedStylesheets: AdoptedStylesheets;

connectedCallback() {
super.connectedCallback();
this.AdoptedStylesheets = new AdoptedStylesheets(globalStyles);
this.AdoptedStylesheets = new AdoptedStylesheets(globalStyles, document);
this.addController(this.AdoptedStylesheets);
}
}
```

In the provided example, the `connectedCallback` method is utilized. This method is invoked whenever the element is inserted into the DOM. Within this method, an instance of `AdoptedStylesheets` is created and added as a controller. This is a more efficient approach than creating the instance and adding the controller within the `constructor`. The reason for this is that it delays these operations until the element is actually inserted into the DOM. If there are many such elements that are created but not immediately added to the DOM, this approach can significantly improve the startup performance of your application. Therefore, the `connectedCallback` method is a crucial part of managing the lifecycle of a web component, especially when dealing with adopted stylesheets.
### Sample 2: Attach a stylesheet to `this.shadowRoot`

```typescript
import { AdoptedStylesheets } from '@phase2/outline-adopted-stylesheets-controller';
import { css, CSSResult, LitElement } from 'lit';
import encapsulatedStyles from './encapsulated-styles';

class MyComponent extends LitElement {
AdoptedStylesheets: AdoptedStylesheets;

connectedCallback() {
super.connectedCallback();
this.AdoptedStylesheets = new AdoptedStylesheets(encapsulatedStyles, this.shadowRoot);
this.addController(this.AdoptedStylesheets);
}
}
```

### Sample 3: Attach stylesheets to both `document` and `this.shadowRoot`

```typescript
import { AdoptedStylesheets } from '@phase2/outline-adopted-stylesheets-controller';
import { css, CSSResult, LitElement } from 'lit';
import globalStyles from './global-styles';
import encapsulatedStyles from './encapsulated-styles';

class MyComponent extends LitElement {
GlobalStylesheets: AdoptedStylesheets;
EncapsulatedStylesheets: AdoptedStylesheets;

connectedCallback() {
super.connectedCallback();
this.GlobalStylesheets = new AdoptedStylesheets(globalStyles, document);
this.addController(this.GlobalStylesheets);
this.EncapsulatedStylesheets = new AdoptedStylesheets(encapsulatedStyles, this.shadowRoot);
this.addController(this.EncapsulatedStylesheets);
}
}
```

In the provided examples, the `connectedCallback` method is utilized. This method is invoked whenever the element is inserted into the DOM. Within this method, an instance of `AdoptedStylesheets` is created and added as a controller. This is a more efficient approach than creating the instance and adding the controller within the `constructor`. The reason for this is that it delays these operations until the element is actually inserted into the DOM. If there are many such elements that are created but not immediately added to the DOM, this approach can significantly improve the startup performance of your application. Therefore, the `connectedCallback` method is a crucial part of managing the lifecycle of a web component, especially when dealing with adopted stylesheets.
44 changes: 34 additions & 10 deletions packages/controllers/adopted-stylesheets/src/adopted-stylesheets.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,57 @@
import { ReactiveController, CSSResult } 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 {Map<string, CSSStyleSheet>} styleSheetMap - A static map that stores CSS stylesheets. The key is the CSS text and the value is the corresponding CSSStyleSheet object.
* @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.
*/
export class AdoptedStyleSheets implements ReactiveController {
private static styleSheetMap = new Map<string, CSSStyleSheet>();
private documentSheet: CSSStyleSheet;
private adoptedSheet: CSSStyleSheet;
private root: Document | ShadowRoot;

constructor(globalStyles: CSSResult) {
/**
* The constructor for the `AdoptedStyleSheets` class.
*
* @param {CSSResult} globalStyles - A `CSSResult` object that contains the CSS styles to be adopted.
* @param {Document | ShadowRoot} root - The root where the stylesheet will be adopted.
*/
constructor(globalStyles: CSSResult, root: Document | ShadowRoot) {
const cssText = globalStyles.cssText;
if (!AdoptedStyleSheets.styleSheetMap.has(cssText)) {
const newSheet = new CSSStyleSheet();
newSheet.replaceSync(cssText);
AdoptedStyleSheets.styleSheetMap.set(cssText, newSheet);
}
this.documentSheet =
this.adoptedSheet =
AdoptedStyleSheets.styleSheetMap.get(cssText) || new CSSStyleSheet();
this.root = root;
himerus marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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.
*/
hostConnected() {
if (!document.adoptedStyleSheets.includes(this.documentSheet)) {
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
this.documentSheet,
if (!this.root.adoptedStyleSheets.includes(this.adoptedSheet)) {
this.root.adoptedStyleSheets = [
...this.root.adoptedStyleSheets,
this.adoptedSheet,
himerus marked this conversation as resolved.
Show resolved Hide resolved
];
}
}

/**
* 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.
*/
hostDisconnected() {
if (document.adoptedStyleSheets.includes(this.documentSheet)) {
document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
sheet => sheet !== this.documentSheet
if (this.root.adoptedStyleSheets.includes(this.adoptedSheet)) {
this.root.adoptedStyleSheets = this.root.adoptedStyleSheets.filter(
sheet => sheet !== this.adoptedSheet
himerus marked this conversation as resolved.
Show resolved Hide resolved
);
}
}
Expand Down