From d941dfcc0a0f5eef84a8d3cd8972f0f358ba438f Mon Sep 17 00:00:00 2001 From: Owen Niblock Date: Mon, 11 Mar 2024 11:51:14 +0000 Subject: [PATCH] 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'