diff --git a/src/index.js b/src/index.js index 4916451..287e6b7 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import { h, cloneElement, render, hydrate } from 'preact'; +import { h, cloneElement, render, hydrate, Fragment } from 'preact'; /** * @typedef {import('./index.d.ts').PreactCustomElement} PreactCustomElement @@ -26,7 +26,9 @@ export default function register(Component, tagName, propNames, options) { } PreactElement.prototype = Object.create(HTMLElement.prototype); PreactElement.prototype.constructor = PreactElement; - PreactElement.prototype.connectedCallback = connectedCallback; + PreactElement.prototype.connectedCallback = function () { + connectedCallback.call(this, options); + }; PreactElement.prototype.attributeChangedCallback = attributeChangedCallback; PreactElement.prototype.disconnectedCallback = disconnectedCallback; @@ -89,7 +91,7 @@ function ContextProvider(props) { /** * @this {PreactCustomElement} */ -function connectedCallback() { +function connectedCallback(options) { // Obtain a reference to the previous context by pinging the nearest // higher up node that was rendered with Preact. If one Preact component // higher up receives our ping, it will set the `detail` property of @@ -106,7 +108,7 @@ function connectedCallback() { this._vdom = h( ContextProvider, { ...this._props, context }, - toVdom(this, this._vdomComponent) + toVdom(this, this._vdomComponent, options) ); (this.hasAttribute('hydrate') ? hydrate : render)(this._vdom, this._root); } @@ -170,10 +172,11 @@ function Slot(props, context) { } } }; - return h('slot', { ...props, ref }); + const { useFragment, ...rest } = props; + return h(useFragment ? Fragment : 'slot', { ...rest, ref }); } -function toVdom(element, nodeName) { +function toVdom(element, nodeName, options) { if (element.nodeType === 3) return element.data; if (element.nodeType !== 1) return null; let children = [], @@ -189,7 +192,7 @@ function toVdom(element, nodeName) { } for (i = cn.length; i--; ) { - const vnode = toVdom(cn[i], null); + const vnode = toVdom(cn[i], null, options); // Move slots correctly const name = cn[i].slot; if (name) { @@ -199,7 +202,15 @@ function toVdom(element, nodeName) { } } + const shadow = !!(options && options.shadow); + // Only wrap the topmost node with a slot - const wrappedChildren = nodeName ? h(Slot, null, children) : children; + const wrappedChildren = nodeName + ? h(Slot, { useFragment: !shadow }, children) + : children; + + if (!shadow && nodeName) { + element.innerHTML = ''; + } return h(nodeName || element.nodeName.toLowerCase(), props, wrappedChildren); } diff --git a/src/index.test.jsx b/src/index.test.jsx index 9609a08..fbbe185 100644 --- a/src/index.test.jsx +++ b/src/index.test.jsx @@ -1,9 +1,21 @@ import { assert } from '@open-wc/testing'; -import { h, createContext, Component } from 'preact'; +import { h, createContext, Component, Fragment } from 'preact'; import { useContext } from 'preact/hooks'; import { act } from 'preact/test-utils'; import registerElement from './index'; +/** @param {string} name */ +function createTestElement(name) { + const el = document.createElement(name); + const child1 = document.createElement('p'); + child1.textContent = 'Child 1'; + const child2 = document.createElement('p'); + child2.textContent = 'Child 2'; + el.appendChild(child1); + el.appendChild(child2); + return el; +} + describe('web components', () => { /** @type {HTMLDivElement} */ let root; @@ -359,4 +371,54 @@ describe('web components', () => { const style = getComputedStyle(child); assert.equal(style.color, 'rgb(255, 0, 0)'); }); + + it('supports controlling light DOM children', async () => { + function LightDomChildren({ children }) { + return ( + +

Light DOM Children

+
{children}
+
+ ); + } + + registerElement(LightDomChildren, 'light-dom-children', []); + registerElement(LightDomChildren, 'light-dom-children-shadow-false', [], { + shadow: false, + }); + + root.appendChild(createTestElement('light-dom-children')); + root.appendChild(createTestElement('light-dom-children-shadow-false')); + + assert.equal( + document.querySelector('light-dom-children').innerHTML, + '

Light DOM Children

Child 1

Child 2

' + ); + assert.equal( + document.querySelector('light-dom-children-shadow-false').innerHTML, + '

Light DOM Children

Child 1

Child 2

' + ); + }); + + it('supports controlling shadow DOM children', () => { + function ShadowDomChildren({ children }) { + return ( + +

Light DOM Children

+
{children}
+
+ ); + } + + registerElement(ShadowDomChildren, 'shadow-dom-children', [], { + shadow: true, + }); + + root.appendChild(createTestElement('shadow-dom-children')); + + assert.equal( + document.querySelector('shadow-dom-children').shadowRoot.innerHTML, + '

Light DOM Children

Child 1

Child 2

' + ); + }); });