Skip to content

Commit

Permalink
Improve style sheet adoption ergonomics.
Browse files Browse the repository at this point in the history
One common pattern for element authors (now that import attributes
enable folks to import css files directly) is to adopt imported style
sheets into a shadow root at custom element initialization time.

This adds two static getters — `shadowRootInit` and `styleSheets`. The
goal is to make shadow root initialization as _declarative_ as possible.
Note that we still expose `createRenderRoot` so we don’t get in the way
if folks need to do something more advanced (it’s also still the only
way to forgo the creation of a shadow root at all).

Closes #52.
  • Loading branch information
theengineear committed Feb 2, 2024
1 parent 8807f5a commit 2604001
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 14 deletions.
38 changes: 30 additions & 8 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,26 +252,48 @@ class MyElement extends XElement {

## Render Root

By default, XElement will create an open shadow root. However, you can change
this behavior by overriding the `createRenderRoot` method. There are a few
reasons why you might want to do this as shown below.
By default, XElement will create an open shadow root with no adopted style
sheets. However, you can change this behavior by overriding the
`shadowRootInit` and `styleSheets` getters. Or, for full control, you can use
the `createRenderRoot` to manually configure or not use a shadow root at all.

### No Shadow Root
### Custom Shadow Root Initialization

Control special behavior like “focus delegation” by overriding the default
shadow root configuration.

```javascript
class MyElement extends XElement {
static createRenderRoot(host) {
return host;
static shadowRootInit() {
return { mode: 'open', delegatesFocus: true };
}
}
```

### Adopted Style Sheets

Import and leverage `.css` files via import attributes. Style sheets returned
by the `styleSheets` getter will be adopted by host’s the attached shadow root.

```javascript
import myElementStyleSheet from './my-element-style.css' with { type: 'css' };

class MyElement extends XElement {
static get styleSheets() {
return [myElementStyleSheet];
}
}
```

### Focus Delegation
### No Shadow Root

Sometimes, you don’t want encapsulation. No problem — just return the `host`
directly by overriding `createRenderRoot`.

```javascript
class MyElement extends XElement {
static createRenderRoot(host) {
return host.attachShadowRoot({ mode: 'open', delegatesFocus: true });
return host;
}
}
```
Expand Down
42 changes: 38 additions & 4 deletions test/test-render-root.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import XElement from '../x-element.js';
import { assert, it } from './x-test.js';

class TestElement extends XElement {
class TestElement1 extends XElement {
static createRenderRoot(host) {
return host;
}
Expand All @@ -11,21 +11,55 @@ class TestElement extends XElement {
};
}
}
customElements.define('test-element', TestElement);
customElements.define('test-element-1', TestElement1);

class TestElement2 extends XElement {
static get styleSheets() {
// TODO: Replace with direct import of css file when better-supported in
// browsers. I.e., use import attributes with { type: 'css' }.
const css = `\
:host {
display: block;
background-color: coral;
width: 100px;
height: 100px;
}
`;
const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(css);
return [styleSheet];
}
static template(html) {
return () => {
return html``;
};
}
}
customElements.define('test-element-2', TestElement2);


it('test render root was respected', () => {
const el = document.createElement('test-element');
const el = document.createElement('test-element-1');
document.body.append(el);
assert(el.shadowRoot === null);
assert(el.textContent === `I'm not in a shadow root.`);
el.remove();
});

it('provided style sheets are adopted', () => {
const el = document.createElement('test-element-2');
document.body.append(el);
const boundingClientRect = el.getBoundingClientRect();
assert(boundingClientRect.width === 100);
assert(boundingClientRect.height === 100);
el.remove();
});

it('errors are thrown in for creating a bad render root', () => {
class BadElement extends XElement {
static createRenderRoot() {}
}
customElements.define('test-element-1', BadElement);
customElements.define('test-element-3', BadElement);
let passed = false;
let message = 'no error was thrown';
try {
Expand Down
2 changes: 2 additions & 0 deletions x-element.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export class XElement extends HTMLElement {
render: (container: HTMLElement, result: any) => void,
html: (strings: TemplateStringsArray, ...any) => any,
}
static readonly shadowRootInit: ShadowRootInit
static readonly styleSheets: [CSSStyleSheet]
static createRenderRoot(host: XElement): HTMLElement;
static template(
html: (strings: TemplateStringsArray, ...any) => any,
Expand Down
25 changes: 23 additions & 2 deletions x-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export default class XElement extends HTMLElement {
return TemplateEngine.interface;
}

/** Configured templating engine. Defaults to "defaultTemplateEngine".
/**
* Configured templating engine. Defaults to "defaultTemplateEngine".
*
* Override this as needed if x-element's default template engine does not
* meet your needs. A "render" method is the only required field. An "html"
Expand All @@ -21,6 +22,24 @@ export default class XElement extends HTMLElement {
return XElement.defaultTemplateEngine;
}

/**
* Declare an initialization object for the host’s shadow root.
* See https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow.
*/
static get shadowRootInit() {
return { mode: 'open' };
}

/**
* Declare an array of CSSSTyleSheet objects to adopt on the shadow root.
* Note that a CSSStyleSheet object is the type returned when importing a
* stylesheet file via import attributes. This has no effect if you are using
* the “host” as your render root (versus attaching a shadow root).
*/
static get styleSheets() {
return [];
}

/**
* Declare watched properties (and related attributes) on an element.
*
Expand Down Expand Up @@ -68,7 +87,9 @@ export default class XElement extends HTMLElement {
* E.g., setup focus delegation or return host instead of host.shadowRoot.
*/
static createRenderRoot(host) {
return host.attachShadow({ mode: 'open' });
const shadowRoot = host.attachShadow(this.shadowRootInit);
shadowRoot.adoptedStyleSheets = this.styleSheets;
return shadowRoot;
}

/**
Expand Down

0 comments on commit 2604001

Please sign in to comment.