From 1d98462135a196b9d9037dd46f0e7fe55d108496 Mon Sep 17 00:00:00 2001 From: Tanner Reits <47483144+tanner-reits@users.noreply.github.com> Date: Tue, 5 Sep 2023 11:03:42 -0400 Subject: [PATCH] fix(runtime): patch methods for scoped slot `append`, `prepend`, and `insertAdjacent` (#4719) * cherry-pick test commit * port over method patches * add tests for additional `insertAdjacent` methods * add e2e tests for patched append/prepend methods * remove previous tests for `appendChild` * patch shadowDOM for custom elements & mark future code deletions w/ todos * update component typedefs from removing test suite * remove some `any` types * keep original methods in patch scope * add co-author for squash Co-authored-by: johnjenkins --------- Co-authored-by: johnjenkins --- src/app-data/index.ts | 6 + src/compiler/app-core/app-data.ts | 6 + .../hydrate-build-conditionals.ts | 5 + src/declarations/stencil-private.ts | 7 + src/declarations/stencil-public-compiler.ts | 4 + src/runtime/bootstrap-custom-element.ts | 26 +++ src/runtime/bootstrap-lazy.ts | 38 +++-- src/runtime/dom-extras.ts | 144 +++++++++++++++- src/testing/reset-build-conditionals.ts | 5 + test/karma/stencil.config.ts | 5 +- test/karma/test-app/append-child/cmp.tsx | 45 ----- test/karma/test-app/append-child/index.html | 40 ----- .../karma/test-app/append-child/karma.spec.ts | 33 ---- test/karma/test-app/components.d.ts | 39 +++-- .../scoped-slot-append-and-prepend/cmp.tsx | 16 ++ .../scoped-slot-append-and-prepend/index.html | 32 ++++ .../karma.spec.ts | 68 ++++++++ .../scoped-slot-child-insert-adjacent/cmp.tsx | 16 ++ .../index.html | 69 ++++++++ .../karma.spec.ts | 159 ++++++++++++++++++ 20 files changed, 610 insertions(+), 153 deletions(-) delete mode 100644 test/karma/test-app/append-child/cmp.tsx delete mode 100644 test/karma/test-app/append-child/index.html delete mode 100644 test/karma/test-app/append-child/karma.spec.ts create mode 100644 test/karma/test-app/scoped-slot-append-and-prepend/cmp.tsx create mode 100644 test/karma/test-app/scoped-slot-append-and-prepend/index.html create mode 100644 test/karma/test-app/scoped-slot-append-and-prepend/karma.spec.ts create mode 100644 test/karma/test-app/scoped-slot-child-insert-adjacent/cmp.tsx create mode 100644 test/karma/test-app/scoped-slot-child-insert-adjacent/index.html create mode 100644 test/karma/test-app/scoped-slot-child-insert-adjacent/karma.spec.ts diff --git a/src/app-data/index.ts b/src/app-data/index.ts index 6ec34f2134b..6d20981ed0e 100644 --- a/src/app-data/index.ts +++ b/src/app-data/index.ts @@ -81,14 +81,18 @@ export const BUILD: BuildConditionals = { lazyLoad: false, profile: false, slotRelocation: true, + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior appendChildSlotFix: false, + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior cloneNodeFix: false, hydratedAttribute: false, hydratedClass: true, scriptDataOpts: false, + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior scopedSlotTextContentFix: false, // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field shadowDomShim: false, + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior slotChildNodesFix: false, invisiblePrehydration: true, propBoolean: true, @@ -103,6 +107,8 @@ export const BUILD: BuildConditionals = { asyncQueue: false, transformTagName: false, attachStyles: true, + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior + patchPseudoShadowDom: false, }; export const Env = {}; diff --git a/src/compiler/app-core/app-data.ts b/src/compiler/app-core/app-data.ts index ba826ed11c0..410da4f3edd 100644 --- a/src/compiler/app-core/app-data.ts +++ b/src/compiler/app-core/app-data.ts @@ -162,10 +162,16 @@ export const updateBuildConditionals = (config: ValidatedConfig, b: BuildConditi b.constructableCSS = !b.hotModuleReplacement || !!config._isTesting; b.asyncLoading = !!(b.asyncLoading || b.lazyLoad || b.taskQueue || b.initializeNextTick); b.cssAnnotations = true; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior b.appendChildSlotFix = config.extras.appendChildSlotFix; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior b.slotChildNodesFix = config.extras.slotChildNodesFix; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior + b.patchPseudoShadowDom = config.extras.experimentalSlotFixes; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior b.cloneNodeFix = config.extras.cloneNodeFix; b.lifecycleDOMEvents = !!(b.isDebug || config._isTesting || config.extras.lifecycleDOMEvents); + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior b.scopedSlotTextContentFix = !!config.extras.scopedSlotTextContentFix; b.scriptDataOpts = config.extras.scriptDataOpts; b.attachStyles = true; diff --git a/src/compiler/output-targets/dist-hydrate-script/hydrate-build-conditionals.ts b/src/compiler/output-targets/dist-hydrate-script/hydrate-build-conditionals.ts index e324e1500cc..26700de22bd 100644 --- a/src/compiler/output-targets/dist-hydrate-script/hydrate-build-conditionals.ts +++ b/src/compiler/output-targets/dist-hydrate-script/hydrate-build-conditionals.ts @@ -19,8 +19,13 @@ export const getHydrateBuildConditionals = (cmps: d.ComponentCompilerMeta[]) => build.member = true; build.constructableCSS = false; build.asyncLoading = true; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior build.appendChildSlotFix = false; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior build.slotChildNodesFix = false; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior + build.patchPseudoShadowDom = false; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior build.cloneNodeFix = false; build.cssAnnotations = true; // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts index ebec17a3658..ad7b4773177 100644 --- a/src/declarations/stencil-private.ts +++ b/src/declarations/stencil-private.ts @@ -166,9 +166,13 @@ export interface BuildConditionals extends Partial { lazyLoad?: boolean; profile?: boolean; constructableCSS?: boolean; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior appendChildSlotFix?: boolean; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior slotChildNodesFix?: boolean; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior scopedSlotTextContentFix?: boolean; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior cloneNodeFix?: boolean; hydratedAttribute?: boolean; hydratedClass?: boolean; @@ -179,6 +183,9 @@ export interface BuildConditionals extends Partial { asyncQueue?: boolean; transformTagName?: boolean; attachStyles?: boolean; + + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior + patchPseudoShadowDom?: boolean; } export type ModuleFormat = diff --git a/src/declarations/stencil-public-compiler.ts b/src/declarations/stencil-public-compiler.ts index aea299ccb53..2d8040ea211 100644 --- a/src/declarations/stencil-public-compiler.ts +++ b/src/declarations/stencil-public-compiler.ts @@ -293,6 +293,7 @@ export interface StencilConfig { } export interface ConfigExtras { + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior /** * By default, the slot polyfill does not update `appendChild()` so that it appends * new child nodes into the correct child slot like how shadow dom works. This is an opt-in @@ -302,6 +303,7 @@ export interface ConfigExtras { */ appendChildSlotFix?: boolean; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior /** * By default, the runtime does not polyfill `cloneNode()` when cloning a component * that uses the slot polyfill. This is an opt-in polyfill for those who need it. @@ -341,6 +343,7 @@ export interface ConfigExtras { */ scriptDataOpts?: boolean; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior /** * Experimental flag to align the behavior of invoking `textContent` on a scoped component to act more like a * component that uses the shadow DOM. Defaults to `false` @@ -355,6 +358,7 @@ export interface ConfigExtras { */ initializeNextTick?: boolean; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior /** * For browsers that do not support shadow dom (IE11 and Edge 18 and below), slot is polyfilled * to simulate the same behavior. However, the host element's `childNodes` and `children` diff --git a/src/runtime/bootstrap-custom-element.ts b/src/runtime/bootstrap-custom-element.ts index 322b8c71886..23d1f5ded96 100644 --- a/src/runtime/bootstrap-custom-element.ts +++ b/src/runtime/bootstrap-custom-element.ts @@ -5,6 +5,13 @@ import { CMP_FLAGS } from '@utils'; import type * as d from '../declarations'; import { connectedCallback } from './connected-callback'; import { disconnectedCallback } from './disconnected-callback'; +import { + patchChildSlotNodes, + patchCloneNode, + patchPseudoShadowDom, + patchSlotAppendChild, + patchTextContent, +} from './dom-extras'; import { computeMode } from './mode'; import { proxyComponent } from './proxy-component'; import { PROXY_FLAGS } from './runtime-constants'; @@ -36,6 +43,25 @@ export const proxyCustomElement = (Cstr: any, compactMeta: d.ComponentRuntimeMet cmpMeta.$flags$ |= CMP_FLAGS.needsShadowDomShim; } + // TODO(STENCIL-914): this check and `else` block can go away and be replaced by just `BUILD.scoped` once we + // default our pseudo-slot behavior + if (BUILD.patchPseudoShadowDom && BUILD.scoped) { + patchPseudoShadowDom(Cstr.prototype, cmpMeta); + } else { + if (BUILD.slotChildNodesFix) { + patchChildSlotNodes(Cstr.prototype, cmpMeta); + } + if (BUILD.cloneNodeFix) { + patchCloneNode(Cstr.prototype); + } + if (BUILD.appendChildSlotFix) { + patchSlotAppendChild(Cstr.prototype); + } + if (BUILD.scopedSlotTextContentFix) { + patchTextContent(Cstr.prototype, cmpMeta); + } + } + const originalConnectedCallback = Cstr.prototype.connectedCallback; const originalDisconnectedCallback = Cstr.prototype.disconnectedCallback; Object.assign(Cstr.prototype, { diff --git a/src/runtime/bootstrap-lazy.ts b/src/runtime/bootstrap-lazy.ts index 4c86aa7e5ef..53ebf740dee 100644 --- a/src/runtime/bootstrap-lazy.ts +++ b/src/runtime/bootstrap-lazy.ts @@ -5,7 +5,13 @@ import { CMP_FLAGS, queryNonceMetaTagContent } from '@utils'; import type * as d from '../declarations'; import { connectedCallback } from './connected-callback'; import { disconnectedCallback } from './disconnected-callback'; -import { patchChildSlotNodes, patchCloneNode, patchSlotAppendChild, patchTextContent } from './dom-extras'; +import { + patchChildSlotNodes, + patchCloneNode, + patchPseudoShadowDom, + patchSlotAppendChild, + patchTextContent, +} from './dom-extras'; import { hmrStart } from './hmr-component'; import { createTime, installDevTools } from './profile'; import { proxyComponent } from './proxy-component'; @@ -108,9 +114,6 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d. (self as any).shadowRoot = self; } } - if (BUILD.slotChildNodesFix) { - patchChildSlotNodes(self, cmpMeta); - } } connectedCallback() { @@ -135,12 +138,23 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d. } }; - if (BUILD.cloneNodeFix) { - patchCloneNode(HostElement.prototype); - } - - if (BUILD.appendChildSlotFix) { - patchSlotAppendChild(HostElement.prototype); + // TODO(STENCIL-914): this check and `else` block can go away and be replaced by just `BUILD.scoped` once we + // default our pseudo-slot behavior + if (BUILD.patchPseudoShadowDom && BUILD.scoped) { + patchPseudoShadowDom(HostElement.prototype, cmpMeta); + } else { + if (BUILD.slotChildNodesFix) { + patchChildSlotNodes(HostElement.prototype, cmpMeta); + } + if (BUILD.cloneNodeFix) { + patchCloneNode(HostElement.prototype); + } + if (BUILD.appendChildSlotFix) { + patchSlotAppendChild(HostElement.prototype); + } + if (BUILD.scopedSlotTextContentFix) { + patchTextContent(HostElement.prototype, cmpMeta); + } } if (BUILD.hotModuleReplacement) { @@ -149,10 +163,6 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d. }; } - if (BUILD.scopedSlotTextContentFix) { - patchTextContent(HostElement.prototype, cmpMeta); - } - cmpMeta.$lazyBundleId$ = lazyBundle[0]; if (!exclude.includes(tagName) && !customElements.get(tagName)) { diff --git a/src/runtime/dom-extras.ts b/src/runtime/dom-extras.ts index 61b82ff27da..36e8c36d4c5 100644 --- a/src/runtime/dom-extras.ts +++ b/src/runtime/dom-extras.ts @@ -6,7 +6,22 @@ import { CMP_FLAGS, HOST_FLAGS } from '@utils'; import type * as d from '../declarations'; import { PLATFORM_FLAGS } from './runtime-constants'; -export const patchCloneNode = (HostElementPrototype: any) => { +export const patchPseudoShadowDom = ( + hostElementPrototype: HTMLElement, + descriptorPrototype: d.ComponentRuntimeMeta, +) => { + patchCloneNode(hostElementPrototype); + patchSlotAppendChild(hostElementPrototype); + patchSlotAppend(hostElementPrototype); + patchSlotPrepend(hostElementPrototype); + patchSlotInsertAdjacentElement(hostElementPrototype); + patchSlotInsertAdjacentHTML(hostElementPrototype); + patchSlotInsertAdjacentText(hostElementPrototype); + patchTextContent(hostElementPrototype, descriptorPrototype); + patchChildSlotNodes(hostElementPrototype, descriptorPrototype); +}; + +export const patchCloneNode = (HostElementPrototype: HTMLElement) => { const orgCloneNode = HostElementPrototype.cloneNode; HostElementPrototype.cloneNode = function (deep?: boolean) { @@ -65,6 +80,127 @@ export const patchSlotAppendChild = (HostElementPrototype: any) => { }; }; +/** + * Patches the `prepend` method for a slotted node inside a scoped component. + * + * @param HostElementPrototype the `Element` to be patched + */ +export const patchSlotPrepend = (HostElementPrototype: HTMLElement) => { + const originalPrepend = HostElementPrototype.prepend; + + HostElementPrototype.prepend = function (this: d.HostElement, ...newChildren: (d.RenderNode | string)[]) { + newChildren.forEach((newChild: d.RenderNode | string) => { + if (typeof newChild === 'string') { + newChild = this.ownerDocument.createTextNode(newChild) as unknown as d.RenderNode; + } + const slotName = (newChild['s-sn'] = getSlotName(newChild)); + const slotNode = getHostSlotNode(this.childNodes, slotName); + if (slotNode) { + const slotPlaceholder: d.RenderNode = document.createTextNode('') as any; + slotPlaceholder['s-nr'] = newChild; + (slotNode['s-cr'].parentNode as any).__appendChild(slotPlaceholder); + newChild['s-ol'] = slotPlaceholder; + + const slotChildNodes = getHostSlotChildNodes(slotNode, slotName); + const appendAfter = slotChildNodes[0]; + return appendAfter.parentNode.insertBefore(newChild, appendAfter.nextSibling); + } + + if (newChild.nodeType === 1 && !!newChild.getAttribute('slot')) { + newChild.hidden = true; + } + + return originalPrepend.call(this, newChild); + }); + }; +}; + +/** + * Patches the `append` method for a slotted node inside a scoped component. The patched method uses + * `appendChild` under-the-hood while creating text nodes for any new children that passed as bare strings. + * + * @param HostElementPrototype the `Element` to be patched + */ +export const patchSlotAppend = (HostElementPrototype: HTMLElement) => { + HostElementPrototype.append = function (this: d.HostElement, ...newChildren: (d.RenderNode | string)[]) { + newChildren.forEach((newChild: d.RenderNode | string) => { + if (typeof newChild === 'string') { + newChild = this.ownerDocument.createTextNode(newChild) as unknown as d.RenderNode; + } + this.appendChild(newChild); + }); + }; +}; + +/** + * Patches the `insertAdjacentHTML` method for a slotted node inside a scoped component. Specifically, + * we only need to patch the behavior for the specific `beforeend` and `afterbegin` positions so the element + * gets inserted into the DOM in the correct location. + * + * @param HostElementPrototype the `Element` to be patched + */ +export const patchSlotInsertAdjacentHTML = (HostElementPrototype: HTMLElement) => { + const originalInsertAdjacentHtml = HostElementPrototype.insertAdjacentHTML; + + HostElementPrototype.insertAdjacentHTML = function (this: d.HostElement, position: InsertPosition, text: string) { + if (position !== 'afterbegin' && position !== 'beforeend') { + return originalInsertAdjacentHtml.call(this, position, text); + } + const container = this.ownerDocument.createElement('_'); + let node: d.RenderNode; + container.innerHTML = text; + + if (position === 'afterbegin') { + while ((node = container.firstChild as d.RenderNode)) { + this.prepend(node); + } + } else if (position === 'beforeend') { + while ((node = container.firstChild as d.RenderNode)) { + this.append(node); + } + } + }; +}; + +/** + * Patches the `insertAdjacentText` method for a slotted node inside a scoped component. Specifically, + * we only need to patch the behavior for the specific `beforeend` and `afterbegin` positions so the text node + * gets inserted into the DOM in the correct location. + * + * @param HostElementPrototype the `Element` to be patched + */ +export const patchSlotInsertAdjacentText = (HostElementPrototype: HTMLElement) => { + HostElementPrototype.insertAdjacentText = function (this: d.HostElement, position: InsertPosition, text: string) { + this.insertAdjacentHTML(position, text); + }; +}; + +/** + * Patches the `insertAdjacentElement` method for a slotted node inside a scoped component. Specifically, + * we only need to patch the behavior for the specific `beforeend` and `afterbegin` positions so the element + * gets inserted into the DOM in the correct location. + * + * @param HostElementPrototype the `Element` to be patched + */ +export const patchSlotInsertAdjacentElement = (HostElementPrototype: HTMLElement) => { + const originalInsertAdjacentElement = HostElementPrototype.insertAdjacentElement; + + HostElementPrototype.insertAdjacentElement = function ( + this: d.HostElement, + position: InsertPosition, + element: d.RenderNode, + ) { + if (position !== 'afterbegin' && position !== 'beforeend') { + return originalInsertAdjacentElement.call(this, position, element); + } + if (position === 'afterbegin') { + this.prepend(element); + } else if (position === 'beforeend') { + this.append(element); + } + }; +}; + /** * Patches the text content of an unnamed slotted node inside a scoped component * @param hostElementPrototype the `Element` to be patched @@ -119,7 +255,7 @@ export const patchTextContent = (hostElementPrototype: HTMLElement, cmpMeta: d.C } }; -export const patchChildSlotNodes = (elm: any, cmpMeta: d.ComponentRuntimeMeta) => { +export const patchChildSlotNodes = (elm: HTMLElement, cmpMeta: d.ComponentRuntimeMeta) => { class FakeNodeList extends Array { item(n: number) { return this[n]; @@ -127,7 +263,7 @@ export const patchChildSlotNodes = (elm: any, cmpMeta: d.ComponentRuntimeMeta) = } // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field if (cmpMeta.$flags$ & CMP_FLAGS.needsShadowDomShim) { - const childNodesFn = elm.__lookupGetter__('childNodes'); + const childNodesFn = (elm as any).__lookupGetter__('childNodes'); Object.defineProperty(elm, 'children', { get() { @@ -143,7 +279,7 @@ export const patchChildSlotNodes = (elm: any, cmpMeta: d.ComponentRuntimeMeta) = Object.defineProperty(elm, 'childNodes', { get() { - const childNodes = childNodesFn.call(this); + const childNodes = childNodesFn.call(this) as NodeListOf; if ( (plt.$flags$ & PLATFORM_FLAGS.isTmpDisconnected) === 0 && getHostRef(this).$flags$ & HOST_FLAGS.hasRendered diff --git a/src/testing/reset-build-conditionals.ts b/src/testing/reset-build-conditionals.ts index a9f8c21db99..cf3d5d034dd 100644 --- a/src/testing/reset-build-conditionals.ts +++ b/src/testing/reset-build-conditionals.ts @@ -46,10 +46,15 @@ export function resetBuildConditionals(b: d.BuildConditionals) { b.hydratedAttribute = false; b.hydratedClass = true; b.invisiblePrehydration = true; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior b.appendChildSlotFix = false; b.cloneNodeFix = false; b.hotModuleReplacement = false; b.scriptDataOpts = false; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior b.scopedSlotTextContentFix = false; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior b.slotChildNodesFix = false; + // TODO(STENCIL-914): remove this option when `experimentalSlotFixes` is the default behavior + b.patchPseudoShadowDom = false; } diff --git a/test/karma/stencil.config.ts b/test/karma/stencil.config.ts index 9bbb4bc478d..d21b1832ed0 100644 --- a/test/karma/stencil.config.ts +++ b/test/karma/stencil.config.ts @@ -29,12 +29,9 @@ export const config: Config = { plugins: [nodePolyfills(), sass()], buildEs5: true, extras: { - appendChildSlotFix: true, - cloneNodeFix: true, lifecycleDOMEvents: true, - scopedSlotTextContentFix: true, scriptDataOpts: true, - slotChildNodesFix: true, + experimentalSlotFixes: true, }, devServer: { // when running `npm start`, serve from the root directory, rather than the `www` output target location diff --git a/test/karma/test-app/append-child/cmp.tsx b/test/karma/test-app/append-child/cmp.tsx deleted file mode 100644 index 9c48827f191..00000000000 --- a/test/karma/test-app/append-child/cmp.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Component, Host, h } from '@stencil/core'; - -@Component({ - tag: 'append-child', - styles: ` - h1 { - color: red; - font-weight: bold; - } - article { - color: green; - font-weight: bold; - } - section { - color: blue; - font-weight: bold; - } - `, - scoped: true, -}) -export class AppendChild { - render() { - return ( - -

- H1 Top - -
H1 Bottom
-

-
- Default Top - - Default Bottom -
-
-
- H6 Top - -
H6 Bottom
-
-
-
- ); - } -} diff --git a/test/karma/test-app/append-child/index.html b/test/karma/test-app/append-child/index.html deleted file mode 100644 index 2a211734933..00000000000 --- a/test/karma/test-app/append-child/index.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - -
- -
- -
- -LightDom - - diff --git a/test/karma/test-app/append-child/karma.spec.ts b/test/karma/test-app/append-child/karma.spec.ts deleted file mode 100644 index f5f5556a302..00000000000 --- a/test/karma/test-app/append-child/karma.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { setupDomTests, waitForChanges } from '../util'; - -describe('append-child', function () { - const { setupDom, tearDownDom } = setupDomTests(document); - let app: HTMLElement; - - beforeEach(async () => { - app = await setupDom('/append-child/index.html'); - }); - afterEach(tearDownDom); - - it('appends to correct slot', async () => { - const btnDefault = app.querySelector('#btnDefault') as HTMLButtonElement; - btnDefault.click(); - btnDefault.click(); - - const btnH1 = app.querySelector('#btnH1') as HTMLButtonElement; - btnH1.click(); - btnH1.click(); - - const btnH6 = app.querySelector('#btnH6') as HTMLButtonElement; - btnH6.click(); - btnH6.click(); - - await waitForChanges(); - - expect(app.querySelector('h1').textContent).toBe('H1 TopH1 Middle 0H1 Middle 1H1 Bottom'); - expect(app.querySelector('article').textContent).toBe( - 'Default TopLightDomDefault Slot 0Default Slot 1Default Bottom', - ); - expect(app.querySelector('section').textContent).toBe('H6 TopH6 Middle 0H6 Middle 1H6 Bottom'); - }); -}); diff --git a/test/karma/test-app/components.d.ts b/test/karma/test-app/components.d.ts index 72ec5a39857..362644c1d06 100644 --- a/test/karma/test-app/components.d.ts +++ b/test/karma/test-app/components.d.ts @@ -10,8 +10,6 @@ import { TestEventDetail } from "./event-custom-type/cmp"; export { SomeTypes } from "./util"; export { TestEventDetail } from "./event-custom-type/cmp"; export namespace Components { - interface AppendChild { - } interface AttributeBasic { "customAttr": string; "multiWord": string; @@ -221,6 +219,10 @@ export namespace Components { } interface ScopedBasicRoot { } + interface ScopedSlotAppendAndPrepend { + } + interface ScopedSlotChildInsertAdjacent { + } interface ShadowDomArray { "values": number[]; } @@ -370,12 +372,6 @@ export interface LifecycleBasicCCustomEvent extends CustomEvent { target: HTMLLifecycleBasicCElement; } declare global { - interface HTMLAppendChildElement extends Components.AppendChild, HTMLStencilElement { - } - var HTMLAppendChildElement: { - prototype: HTMLAppendChildElement; - new (): HTMLAppendChildElement; - }; interface HTMLAttributeBasicElement extends Components.AttributeBasic, HTMLStencilElement { } var HTMLAttributeBasicElement: { @@ -862,6 +858,18 @@ declare global { prototype: HTMLScopedBasicRootElement; new (): HTMLScopedBasicRootElement; }; + interface HTMLScopedSlotAppendAndPrependElement extends Components.ScopedSlotAppendAndPrepend, HTMLStencilElement { + } + var HTMLScopedSlotAppendAndPrependElement: { + prototype: HTMLScopedSlotAppendAndPrependElement; + new (): HTMLScopedSlotAppendAndPrependElement; + }; + interface HTMLScopedSlotChildInsertAdjacentElement extends Components.ScopedSlotChildInsertAdjacent, HTMLStencilElement { + } + var HTMLScopedSlotChildInsertAdjacentElement: { + prototype: HTMLScopedSlotChildInsertAdjacentElement; + new (): HTMLScopedSlotChildInsertAdjacentElement; + }; interface HTMLShadowDomArrayElement extends Components.ShadowDomArray, HTMLStencilElement { } var HTMLShadowDomArrayElement: { @@ -1175,7 +1183,6 @@ declare global { new (): HTMLTag88Element; }; interface HTMLElementTagNameMap { - "append-child": HTMLAppendChildElement; "attribute-basic": HTMLAttributeBasicElement; "attribute-basic-root": HTMLAttributeBasicRootElement; "attribute-boolean": HTMLAttributeBooleanElement; @@ -1257,6 +1264,8 @@ declare global { "sass-cmp": HTMLSassCmpElement; "scoped-basic": HTMLScopedBasicElement; "scoped-basic-root": HTMLScopedBasicRootElement; + "scoped-slot-append-and-prepend": HTMLScopedSlotAppendAndPrependElement; + "scoped-slot-child-insert-adjacent": HTMLScopedSlotChildInsertAdjacentElement; "shadow-dom-array": HTMLShadowDomArrayElement; "shadow-dom-array-root": HTMLShadowDomArrayRootElement; "shadow-dom-basic": HTMLShadowDomBasicElement; @@ -1312,8 +1321,6 @@ declare global { } } declare namespace LocalJSX { - interface AppendChild { - } interface AttributeBasic { "customAttr"?: string; "multiWord"?: string; @@ -1530,6 +1537,10 @@ declare namespace LocalJSX { } interface ScopedBasicRoot { } + interface ScopedSlotAppendAndPrepend { + } + interface ScopedSlotChildInsertAdjacent { + } interface ShadowDomArray { "values"?: number[]; } @@ -1650,7 +1661,6 @@ declare namespace LocalJSX { interface Tag88 { } interface IntrinsicElements { - "append-child": AppendChild; "attribute-basic": AttributeBasic; "attribute-basic-root": AttributeBasicRoot; "attribute-boolean": AttributeBoolean; @@ -1732,6 +1742,8 @@ declare namespace LocalJSX { "sass-cmp": SassCmp; "scoped-basic": ScopedBasic; "scoped-basic-root": ScopedBasicRoot; + "scoped-slot-append-and-prepend": ScopedSlotAppendAndPrepend; + "scoped-slot-child-insert-adjacent": ScopedSlotChildInsertAdjacent; "shadow-dom-array": ShadowDomArray; "shadow-dom-array-root": ShadowDomArrayRoot; "shadow-dom-basic": ShadowDomBasic; @@ -1790,7 +1802,6 @@ export { LocalJSX as JSX }; declare module "@stencil/core" { export namespace JSX { interface IntrinsicElements { - "append-child": LocalJSX.AppendChild & JSXBase.HTMLAttributes; "attribute-basic": LocalJSX.AttributeBasic & JSXBase.HTMLAttributes; "attribute-basic-root": LocalJSX.AttributeBasicRoot & JSXBase.HTMLAttributes; "attribute-boolean": LocalJSX.AttributeBoolean & JSXBase.HTMLAttributes; @@ -1872,6 +1883,8 @@ declare module "@stencil/core" { "sass-cmp": LocalJSX.SassCmp & JSXBase.HTMLAttributes; "scoped-basic": LocalJSX.ScopedBasic & JSXBase.HTMLAttributes; "scoped-basic-root": LocalJSX.ScopedBasicRoot & JSXBase.HTMLAttributes; + "scoped-slot-append-and-prepend": LocalJSX.ScopedSlotAppendAndPrepend & JSXBase.HTMLAttributes; + "scoped-slot-child-insert-adjacent": LocalJSX.ScopedSlotChildInsertAdjacent & JSXBase.HTMLAttributes; "shadow-dom-array": LocalJSX.ShadowDomArray & JSXBase.HTMLAttributes; "shadow-dom-array-root": LocalJSX.ShadowDomArrayRoot & JSXBase.HTMLAttributes; "shadow-dom-basic": LocalJSX.ShadowDomBasic & JSXBase.HTMLAttributes; diff --git a/test/karma/test-app/scoped-slot-append-and-prepend/cmp.tsx b/test/karma/test-app/scoped-slot-append-and-prepend/cmp.tsx new file mode 100644 index 00000000000..cd384a50e6e --- /dev/null +++ b/test/karma/test-app/scoped-slot-append-and-prepend/cmp.tsx @@ -0,0 +1,16 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'scoped-slot-append-and-prepend', + scoped: true, +}) +export class ScopedSlotAppendAndPrepend { + render() { + return ( +
+ Here is my slot. It is red. + +
+ ); + } +} diff --git a/test/karma/test-app/scoped-slot-append-and-prepend/index.html b/test/karma/test-app/scoped-slot-append-and-prepend/index.html new file mode 100644 index 00000000000..0cddb9143dd --- /dev/null +++ b/test/karma/test-app/scoped-slot-append-and-prepend/index.html @@ -0,0 +1,32 @@ + + + + + + +

My initial slotted content.

+
+ + + + + + diff --git a/test/karma/test-app/scoped-slot-append-and-prepend/karma.spec.ts b/test/karma/test-app/scoped-slot-append-and-prepend/karma.spec.ts new file mode 100644 index 00000000000..ae95e14a9ac --- /dev/null +++ b/test/karma/test-app/scoped-slot-append-and-prepend/karma.spec.ts @@ -0,0 +1,68 @@ +import { setupDomTests, waitForChanges } from '../util'; + +describe('scoped-slot-append-and-prepend', () => { + const { setupDom, tearDownDom } = setupDomTests(document); + + let app: HTMLElement | undefined; + let host: HTMLElement | undefined; + let parentDiv: HTMLDivElement | undefined; + + beforeEach(async () => { + app = await setupDom('/scoped-slot-append-and-prepend/index.html'); + host = app.querySelector('scoped-slot-append-and-prepend'); + parentDiv = host.querySelector('#parentDiv'); + }); + + afterEach(tearDownDom); + + describe('append', () => { + it('inserts a DOM element at the end of the slot', async () => { + expect(host).toBeDefined(); + + expect(parentDiv).toBeDefined(); + expect(parentDiv.children.length).toBe(1); + expect(parentDiv.children[0].textContent).toBe('My initial slotted content.'); + + const addButton = app.querySelector('#appendNodes'); + addButton.click(); + await waitForChanges(); + + expect(parentDiv.children.length).toBe(2); + expect(parentDiv.children[1].textContent).toBe('The new slotted content.'); + }); + }); + + describe('appendChild', () => { + it('inserts a DOM element at the end of the slot', async () => { + expect(host).toBeDefined(); + + expect(parentDiv).toBeDefined(); + expect(parentDiv.children.length).toBe(1); + expect(parentDiv.children[0].textContent).toBe('My initial slotted content.'); + + const addButton = app.querySelector('#appendChildNodes'); + addButton.click(); + await waitForChanges(); + + expect(parentDiv.children.length).toBe(2); + expect(parentDiv.children[1].textContent).toBe('The new slotted content.'); + }); + }); + + describe('prepend', () => { + it('inserts a DOM element at the start of the slot', async () => { + expect(host).toBeDefined(); + + expect(parentDiv).toBeDefined(); + expect(parentDiv.children.length).toBe(1); + expect(parentDiv.children[0].textContent).toBe('My initial slotted content.'); + + const addButton = app.querySelector('#prependNodes'); + addButton.click(); + await waitForChanges(); + + expect(parentDiv.children.length).toBe(2); + expect(parentDiv.children[0].textContent).toBe('The new slotted content.'); + }); + }); +}); diff --git a/test/karma/test-app/scoped-slot-child-insert-adjacent/cmp.tsx b/test/karma/test-app/scoped-slot-child-insert-adjacent/cmp.tsx new file mode 100644 index 00000000000..5da5b94d955 --- /dev/null +++ b/test/karma/test-app/scoped-slot-child-insert-adjacent/cmp.tsx @@ -0,0 +1,16 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'scoped-slot-child-insert-adjacent', + scoped: true, +}) +export class ScopedSlotChildInsertAdjacent { + render() { + return ( +
+ Here is my slot. It is red. + +
+ ); + } +} diff --git a/test/karma/test-app/scoped-slot-child-insert-adjacent/index.html b/test/karma/test-app/scoped-slot-child-insert-adjacent/index.html new file mode 100644 index 00000000000..17d77027b78 --- /dev/null +++ b/test/karma/test-app/scoped-slot-child-insert-adjacent/index.html @@ -0,0 +1,69 @@ + + + + + + +

I am slotted and will receive a red background

+
+ + + + + + + + + + + diff --git a/test/karma/test-app/scoped-slot-child-insert-adjacent/karma.spec.ts b/test/karma/test-app/scoped-slot-child-insert-adjacent/karma.spec.ts new file mode 100644 index 00000000000..0668d6b0ac8 --- /dev/null +++ b/test/karma/test-app/scoped-slot-child-insert-adjacent/karma.spec.ts @@ -0,0 +1,159 @@ +import { setupDomTests, waitForChanges } from '../util'; + +describe('scoped-slot-child-insert-adjacent', () => { + const { setupDom, tearDownDom } = setupDomTests(document); + + let app: HTMLElement | undefined; + let host: HTMLElement | undefined; + let parentDiv: HTMLDivElement | undefined; + + beforeEach(async () => { + app = await setupDom('/scoped-slot-child-insert-adjacent/index.html'); + host = app.querySelector('scoped-slot-child-insert-adjacent'); + parentDiv = host.querySelector('#parentDiv'); + }); + + afterEach(tearDownDom); + + describe('insertAdjacentHtml', () => { + it('slots elements w/ "beforeend" position', async () => { + expect(parentDiv).toBeDefined(); + + // before we hit the button to call `insertAdjacentHTML`, we should only have one

elm + let paragraphElms = host.querySelectorAll('p'); + const firstParagraph = paragraphElms[0]; + expect(firstParagraph.textContent).toBe('I am slotted and will receive a red background'); + expect(firstParagraph.parentElement).toBe(parentDiv); + expect((getComputedStyle(parentDiv) as any)['background-color']).toBe('rgb(255, 0, 0)'); + + // insert an additional

elm + const addButton = app.querySelector('#addInsertAdjacentHtmlBeforeEnd'); + addButton.click(); + await waitForChanges(); + + // now we should have 2

elms + paragraphElms = host.querySelectorAll('p'); + expect(paragraphElms.length).toBe(2); + + // the inserted elm should: + // 1. have the

as it's parent + // 2. the
should have the same style (which gets acquired by both

elms) + const secondParagraph = paragraphElms[1]; + expect(secondParagraph.textContent).toBe( + 'Added via insertAdjacentHTMLBeforeEnd. I should have a red background.', + ); + expect(secondParagraph.parentElement).toBe(parentDiv); + expect((getComputedStyle(parentDiv) as any)['background-color']).toBe('rgb(255, 0, 0)'); + }); + + it('slots elements w/ "afterbegin" position', async () => { + expect(parentDiv).toBeDefined(); + + // before we hit the button to call `insertAdjacentHTML`, we should only have one

elm + let paragraphElms = host.querySelectorAll('p'); + const firstParagraph = paragraphElms[0]; + expect(firstParagraph.textContent).toBe('I am slotted and will receive a red background'); + expect(firstParagraph.parentElement).toBe(parentDiv); + expect((getComputedStyle(parentDiv) as any)['background-color']).toBe('rgb(255, 0, 0)'); + + // insert an additional

elm + const addButton = app.querySelector('#addInsertAdjacentHtmlAfterBegin'); + addButton.click(); + await waitForChanges(); + + // now we should have 2

elms + paragraphElms = host.querySelectorAll('p'); + expect(paragraphElms.length).toBe(2); + + // the inserted elm should: + // 1. have the

as it's parent + // 2. the
should have the same style (which gets acquired by both

elms) + const insertedParagraph = paragraphElms[0]; + expect(insertedParagraph.textContent).toBe( + 'Added via insertAdjacentHTMLAfterBegin. I should have a red background.', + ); + expect(insertedParagraph.parentElement).toBe(parentDiv); + expect((getComputedStyle(parentDiv) as any)['background-color']).toBe('rgb(255, 0, 0)'); + }); + }); + + describe('insertAdjacentText', () => { + it('slots elements w/ "beforeend" position', async () => { + expect(parentDiv).toBeDefined(); + + expect(parentDiv.textContent).toBe( + 'Here is my slot. It is red.\n I am slotted and will receive a red background\n', + ); + expect((getComputedStyle(parentDiv) as any)['background-color']).toBe('rgb(255, 0, 0)'); + + // insert an additional text node + const addButton = app.querySelector('#addInsertAdjacentTextBeforeEnd'); + addButton.click(); + await waitForChanges(); + + expect(parentDiv.textContent).toBe( + 'Here is my slot. It is red.\n I am slotted and will receive a red background\nAdded via insertAdjacentTextBeforeEnd. I should have a red background.', + ); + expect((getComputedStyle(parentDiv) as any)['background-color']).toBe('rgb(255, 0, 0)'); + }); + + it('slots elements w/ "afterbegin" position', async () => { + expect(parentDiv).toBeDefined(); + + expect(parentDiv.textContent).toBe( + 'Here is my slot. It is red.\n I am slotted and will receive a red background\n', + ); + expect((getComputedStyle(parentDiv) as any)['background-color']).toBe('rgb(255, 0, 0)'); + + // insert an additional text node + const addButton = app.querySelector('#addInsertAdjacentTextAfterBegin'); + addButton.click(); + await waitForChanges(); + + expect(parentDiv.textContent).toBe( + 'Here is my slot. It is red.Added via insertAdjacentTextAfterBegin. I should have a red background.\n I am slotted and will receive a red background\n', + ); + expect((getComputedStyle(parentDiv) as any)['background-color']).toBe('rgb(255, 0, 0)'); + }); + }); + + describe('insertAdjacentElement', () => { + it('slots elements w/ "beforeend" position', async () => { + expect(parentDiv).toBeDefined(); + + let children = parentDiv.children; + expect(children.length).toBe(1); + expect(children[0].textContent).toBe('I am slotted and will receive a red background'); + expect((getComputedStyle(parentDiv) as any)['background-color']).toBe('rgb(255, 0, 0)'); + + const addButton = app.querySelector('#addInsertAdjacentElementBeforeEnd'); + addButton.click(); + await waitForChanges(); + + children = parentDiv.children; + expect(children.length).toBe(2); + expect(children[1].textContent).toBe('Added via insertAdjacentElementBeforeEnd. I should have a red background.'); + expect((getComputedStyle(parentDiv) as any)['background-color']).toBe('rgb(255, 0, 0)'); + }); + + it('slots elements w/ "afterBegin" position', async () => { + expect(parentDiv).toBeDefined(); + + let children = parentDiv.children; + expect(children.length).toBe(1); + expect(children[0].textContent).toBe('I am slotted and will receive a red background'); + expect((getComputedStyle(parentDiv) as any)['background-color']).toBe('rgb(255, 0, 0)'); + + const addButton = app.querySelector('#addInsertAdjacentElementAfterBegin'); + addButton.click(); + await waitForChanges(); + + children = parentDiv.children; + expect(children.length).toBe(2); + expect(children[0].textContent).toBe( + 'Added via insertAdjacentElementAfterBegin. I should have a red background.', + ); + expect((getComputedStyle(parentDiv) as any)['background-color']).toBe('rgb(255, 0, 0)'); + }); + }); +});