From d941dfcc0a0f5eef84a8d3cd8972f0f358ba438f Mon Sep 17 00:00:00 2001 From: Owen Niblock Date: Mon, 11 Mar 2024 11:51:14 +0000 Subject: [PATCH 1/3] In progress noodling --- custom-elements.json | 38 +++++++++++++++ examples/index.html | 6 +-- src/tab-container-element.ts | 90 +++++++++++++++++++++++++----------- 3 files changed, 104 insertions(+), 30 deletions(-) diff --git a/custom-elements.json b/custom-elements.json index bb7e766..ac3dfb8 100644 --- a/custom-elements.json +++ b/custom-elements.json @@ -153,6 +153,21 @@ } ] }, + { + "kind": "method", + "name": "renderShadow", + "static": true + }, + { + "kind": "method", + "name": "setCSPTrustedTypesPolicy", + "static": true, + "parameters": [ + { + "name": "policy" + } + ] + }, { "kind": "field", "name": "onChange" @@ -377,6 +392,29 @@ } ] }, + { + "kind": "method", + "name": "renderShadow", + "static": true + }, + { + "kind": "method", + "name": "setCSPTrustedTypesPolicy", + "static": true, + "return": { + "type": { + "text": "void" + } + }, + "parameters": [ + { + "name": "policy", + "type": { + "text": "CSPTrustedTypesPolicy | Promise | null" + } + } + ] + }, { "kind": "field", "name": "onChange" diff --git a/examples/index.html b/examples/index.html index 4e3553e..3243832 100644 --- a/examples/index.html +++ b/examples/index.html @@ -102,7 +102,7 @@

Set initially selected tab

Set default tab

- + @@ -140,7 +140,7 @@

Panel with extra buttons

- - + + diff --git a/src/tab-container-element.ts b/src/tab-container-element.ts index c01164d..9a10cae 100644 --- a/src/tab-container-element.ts +++ b/src/tab-container-element.ts @@ -1,5 +1,42 @@ +// CSP trusted types: We don't want to add `@types/trusted-types` as a +// dependency, so we use the following types as a stand-in. +interface CSPTrustedTypesPolicy { + createHTML: (s: string) => CSPTrustedHTMLToStringable +} +// Note: basically every object (and some primitives) in JS satisfy this +// `CSPTrustedHTMLToStringable` interface, but this is the most compatible shape +// we can use. +interface CSPTrustedHTMLToStringable { + toString: () => string +} + const HTMLElement = globalThis.HTMLElement || (null as unknown as (typeof window)['HTMLElement']) const manualSlotsSupported = 'assign' in (globalThis.HTMLSlotElement?.prototype || {}) +const html = String.raw + +const shadowHTML = html` +
+ +
+ +
+ +
+ + +` + +export interface ElementRender { + renderShadow(): string + shadowRootOptions?: { + shadowrootmode?: 'open' | 'closed', + delegatesFocus?: boolean, + } +} + +export interface CSPRenderer { + setCSPTrustedTypesPolicy(policy: CSPTrustedTypesPolicy | Promise | null): void +} export class TabContainerChangeEvent extends Event { constructor(type: string, {tab, panel, ...init}: EventInit & {tab?: Element; panel?: Element}) { @@ -25,12 +62,29 @@ export class TabContainerChangeEvent extends Event { } } +let cspTrustedTypesPolicyPromise: Promise | null = null + export class TabContainerElement extends HTMLElement { static define(tag = 'tab-container', registry = customElements) { registry.define(tag, this) return this } + static observedAttributes = ['vertical'] + + static renderShadow() { + return shadowHTML + } + + static shadowRootOptions = { + shadowrootmode: 'open' + } + + // Passing `null` clears the policy. + static setCSPTrustedTypesPolicy(policy: CSPTrustedTypesPolicy | Promise | null): void { + cspTrustedTypesPolicyPromise = policy === null ? policy : Promise.resolve(policy) + } + get onChange() { return this.onTabContainerChange } @@ -83,8 +137,6 @@ export class TabContainerElement extends HTMLElement { this.onTabContainerChanged = listener } - static observedAttributes = ['vertical'] - get #tabList() { const slot = this.#tabListSlot if (this.#tabListTabWrapper.hasAttribute('role')) { @@ -150,33 +202,17 @@ export class TabContainerElement extends HTMLElement { #setupComplete = false #internals!: ElementInternals | null - connectedCallback(): void { + async connectedCallback(): Promise { this.#internals ||= this.attachInternals ? this.attachInternals() : null const shadowRoot = this.shadowRoot || this.attachShadow({mode: 'open', slotAssignment: 'manual'}) - const tabListContainer = document.createElement('div') - tabListContainer.style.display = 'flex' - tabListContainer.setAttribute('part', 'tablist-wrapper') - const tabListTabWrapper = document.createElement('div') - tabListTabWrapper.setAttribute('part', 'tablist-tab-wrapper') - const tabListSlot = document.createElement('slot') - tabListSlot.setAttribute('part', 'tablist') - tabListSlot.setAttribute('name', 'tablist') - tabListTabWrapper.append(tabListSlot) - const panelSlot = document.createElement('slot') - panelSlot.setAttribute('part', 'panel') - panelSlot.setAttribute('name', 'panel') - panelSlot.setAttribute('role', 'presentation') - const beforeTabSlot = document.createElement('slot') - beforeTabSlot.setAttribute('part', 'before-tabs') - beforeTabSlot.setAttribute('name', 'before-tabs') - const afterTabSlot = document.createElement('slot') - afterTabSlot.setAttribute('part', 'after-tabs') - afterTabSlot.setAttribute('name', 'after-tabs') - tabListContainer.append(beforeTabSlot, tabListTabWrapper, afterTabSlot) - const afterSlot = document.createElement('slot') - afterSlot.setAttribute('part', 'after-panels') - afterSlot.setAttribute('name', 'after-panels') - shadowRoot.replaceChildren(tabListContainer, panelSlot, afterSlot) + if (cspTrustedTypesPolicyPromise) { + const cspTrustedTypesPolicy = await cspTrustedTypesPolicyPromise + // eslint-disable-next-line github/no-inner-html + shadowRoot.innerHTML = cspTrustedTypesPolicy.createHTML(shadowHTML).toString() + } else { + // eslint-disable-next-line github/no-inner-html + shadowRoot.innerHTML = shadowHTML + } if (this.#internals && 'role' in this.#internals) { this.#internals.role = 'presentation' From 95b3c46586c29ff9d7dcf324a0ba67ec30a42a3b Mon Sep 17 00:00:00 2001 From: Owen Niblock Date: Wed, 20 Mar 2024 16:44:54 +0000 Subject: [PATCH 2/3] Add some tests --- custom-elements.json | 9 +++++++++ src/tab-container-element.ts | 8 ++++---- test/test.js | 19 ++++++++++++++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/custom-elements.json b/custom-elements.json index d18aeef..5da1cbe 100644 --- a/custom-elements.json +++ b/custom-elements.json @@ -419,6 +419,15 @@ "name": "renderShadow", "static": true }, + { + "kind": "field", + "name": "shadowRootOptions", + "type": { + "text": "object" + }, + "static": true, + "default": "{\n shadowrootmode: 'open',\n }" + }, { "kind": "method", "name": "setCSPTrustedTypesPolicy", diff --git a/src/tab-container-element.ts b/src/tab-container-element.ts index 41a99c7..8ee5b79 100644 --- a/src/tab-container-element.ts +++ b/src/tab-container-element.ts @@ -29,8 +29,8 @@ const shadowHTML = html` export interface ElementRender { renderShadow(): string shadowRootOptions?: { - shadowrootmode?: 'open' | 'closed', - delegatesFocus?: boolean, + shadowrootmode?: 'open' | 'closed' + delegatesFocus?: boolean } } @@ -84,9 +84,9 @@ export class TabContainerElement extends HTMLElement { static renderShadow() { return shadowHTML } - + static shadowRootOptions = { - shadowrootmode: 'open' + shadowrootmode: 'open', } // Passing `null` clears the policy. diff --git a/test/test.js b/test/test.js index 441aa41..c3cc5a9 100644 --- a/test/test.js +++ b/test/test.js @@ -1,5 +1,5 @@ import {assert, expect} from '@open-wc/testing' -import '../src/index.ts' +import TabContainerElement from '../src/index.ts' describe('tab-container', function () { const isSelected = e => e.matches('[aria-selected=true]') @@ -9,6 +9,23 @@ describe('tab-container', function () { let panels = [] let events = [] + describe('Shadow DOM', function () { + it('`renderShadow` contains the correct string representation', function () { + const expected = ` +
+ +
+ +
+ +
+ + +` + assert.equal(TabContainerElement.renderShadow(), expected) + }) + }) + describe('element creation', function () { it('creates from document.createElement', function () { const el = document.createElement('tab-container') From c9dca8863ac380d378275ffb5201f4d2dc7cd997 Mon Sep 17 00:00:00 2001 From: Owen Niblock Date: Fri, 22 Mar 2024 10:57:01 +0000 Subject: [PATCH 3/3] Linting --- src/tab-container-element.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tab-container-element.ts b/src/tab-container-element.ts index 8ee5b79..41a99c7 100644 --- a/src/tab-container-element.ts +++ b/src/tab-container-element.ts @@ -29,8 +29,8 @@ const shadowHTML = html` export interface ElementRender { renderShadow(): string shadowRootOptions?: { - shadowrootmode?: 'open' | 'closed' - delegatesFocus?: boolean + shadowrootmode?: 'open' | 'closed', + delegatesFocus?: boolean, } } @@ -84,9 +84,9 @@ export class TabContainerElement extends HTMLElement { static renderShadow() { return shadowHTML } - + static shadowRootOptions = { - shadowrootmode: 'open', + shadowrootmode: 'open' } // Passing `null` clears the policy. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy