From 260400112b5537b6d73b6f1d432e82d96ec8cfc2 Mon Sep 17 00:00:00 2001 From: Andrew Seier Date: Fri, 2 Feb 2024 13:38:13 -0800 Subject: [PATCH] Improve style sheet adoption ergonomics. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- SPEC.md | 38 ++++++++++++++++++++++++++++-------- test/test-render-root.js | 42 ++++++++++++++++++++++++++++++++++++---- x-element.d.ts | 2 ++ x-element.js | 25 ++++++++++++++++++++++-- 4 files changed, 93 insertions(+), 14 deletions(-) diff --git a/SPEC.md b/SPEC.md index 81ced85..e61e210 100644 --- a/SPEC.md +++ b/SPEC.md @@ -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; } } ``` diff --git a/test/test-render-root.js b/test/test-render-root.js index 12dcee9..886749c 100644 --- a/test/test-render-root.js +++ b/test/test-render-root.js @@ -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; } @@ -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 { diff --git a/x-element.d.ts b/x-element.d.ts index 50cac1a..3fe5ac4 100644 --- a/x-element.d.ts +++ b/x-element.d.ts @@ -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, diff --git a/x-element.js b/x-element.js index 8a9d215..ee6b92a 100644 --- a/x-element.js +++ b/x-element.js @@ -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" @@ -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. * @@ -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; } /**