diff --git a/custom-elements.json b/custom-elements.json index fe0601c..5da1cbe 100644 --- a/custom-elements.json +++ b/custom-elements.json @@ -158,6 +158,21 @@ } ] }, + { + "kind": "method", + "name": "renderShadow", + "static": true + }, + { + "kind": "method", + "name": "setCSPTrustedTypesPolicy", + "static": true, + "parameters": [ + { + "name": "policy" + } + ] + }, { "kind": "field", "name": "onChange" @@ -399,6 +414,38 @@ } ] }, + { + "kind": "method", + "name": "renderShadow", + "static": true + }, + { + "kind": "field", + "name": "shadowRootOptions", + "type": { + "text": "object" + }, + "static": true, + "default": "{\n shadowrootmode: 'open',\n }" + }, + { + "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 831409d..41a99c7 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( @@ -34,12 +71,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 } @@ -92,8 +146,6 @@ export class TabContainerElement extends HTMLElement { this.onTabContainerChanged = listener } - static observedAttributes = ['vertical'] - get #tabList() { const slot = this.#tabListSlot if (this.#tabListTabWrapper.hasAttribute('role')) { @@ -159,33 +211,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' 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') 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