From a60bb6d5fc29fb5b175f583106cc11e375b9fe92 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 23 May 2025 11:32:00 +0800 Subject: [PATCH 1/2] fix(custom-element): inject child style before parent component's style --- packages/runtime-core/src/component.ts | 2 +- packages/runtime-core/src/renderer.ts | 3 +- .../__tests__/customElement.spec.ts | 61 +++++++++++++++++++ packages/runtime-dom/src/apiCustomElement.ts | 34 ++++++++++- 4 files changed, 95 insertions(+), 5 deletions(-) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index f191c36df12..2c73e0fa308 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -1256,7 +1256,7 @@ export interface ComponentCustomElementInterface { /** * @internal */ - _injectChildStyle(type: ConcreteComponent): void + _injectChildStyle(type: ConcreteComponent, parent?: ConcreteComponent): void /** * @internal */ diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 7b39aa917a2..6e31cb01013 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1349,7 +1349,8 @@ function baseCreateRenderer( } else { // custom element style injection if (root.ce) { - root.ce._injectChildStyle(type) + const parent = instance.parent ? instance.parent.type : undefined + root.ce._injectChildStyle(type, parent) } if (__DEV__) { diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index eee2151716e..09d4b0bdee5 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -916,6 +916,67 @@ describe('defineCustomElement', () => { assertStyles(el, [`div { color: blue; }`, `div { color: red; }`]) }) + test('inject child component styles before parent styles', async () => { + const Baz = () => h(Bar) + const Bar = defineComponent({ + styles: [`div { color: green; }`], + render() { + return 'bar' + }, + }) + const WrapperBar = defineComponent({ + styles: [`div { color: blue; }`], + render() { + return h(Baz) + }, + }) + const WBaz = () => h(WrapperBar) + const Foo = defineCustomElement({ + styles: [`div { color: red; }`], + render() { + return [h(Baz), h(WBaz)] + }, + }) + customElements.define('my-el-with-wrapper-child-styles', Foo) + container.innerHTML = `` + const el = container.childNodes[0] as VueElement + + // inject order should be child -> parent + assertStyles(el, [ + `div { color: green; }`, + `div { color: blue; }`, + `div { color: red; }`, + ]) + }) + + test('inject child component styles when parent has no styles', async () => { + const Baz = () => h(Bar) + const Bar = defineComponent({ + styles: [`div { color: green; }`], + render() { + return 'bar' + }, + }) + const WrapperBar = defineComponent({ + styles: [`div { color: blue; }`], + render() { + return h(Baz) + }, + }) + const WBaz = () => h(WrapperBar) + // without styles + const Foo = defineCustomElement({ + render() { + return [h(Baz), h(WBaz)] + }, + }) + customElements.define('my-el-with-inject-child-styles', Foo) + container.innerHTML = `` + const el = container.childNodes[0] as VueElement + + assertStyles(el, [`div { color: green; }`, `div { color: blue; }`]) + }) + test('with nonce', () => { const Foo = defineCustomElement( { diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 56b86a5fd9e..15b3c33d878 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -232,6 +232,7 @@ export class VueElement private _styleChildren = new WeakSet() private _pendingResolve: Promise | undefined private _parent: VueElement | undefined + private _styleAnchor?: HTMLStyleElement | Text /** * dev only */ @@ -584,6 +585,7 @@ export class VueElement private _applyStyles( styles: string[] | undefined, owner?: ConcreteComponent, + parentComp?: ConcreteComponent & CustomElementOptions, ) { if (!styles) return if (owner) { @@ -592,12 +594,35 @@ export class VueElement } this._styleChildren.add(owner) } + + // if parent has no styles but child does, create an anchor + // to inject child styles before it. + if (parentComp && !parentComp.styles) { + const anchor = document.createTextNode('') + if (this._styleAnchor) { + this.shadowRoot!.insertBefore(anchor, this._styleAnchor) + } else { + this.shadowRoot!.prepend(anchor) + } + this._styleAnchor = anchor + } + const nonce = this._nonce + let last = undefined for (let i = styles.length - 1; i >= 0; i--) { const s = document.createElement('style') if (nonce) s.setAttribute('nonce', nonce) s.textContent = styles[i] - this.shadowRoot!.prepend(s) + + // inject styles before parent styles + if (parentComp) { + this.shadowRoot!.insertBefore(s, last || this._styleAnchor!) + } else { + this.shadowRoot!.prepend(s) + this._styleAnchor = s + } + last = s + // record for HMR if (__DEV__) { if (owner) { @@ -665,8 +690,11 @@ export class VueElement /** * @internal */ - _injectChildStyle(comp: ConcreteComponent & CustomElementOptions): void { - this._applyStyles(comp.styles, comp) + _injectChildStyle( + comp: ConcreteComponent & CustomElementOptions, + parentComp?: ConcreteComponent, + ): void { + this._applyStyles(comp.styles, comp, parentComp) } /** From 41379c4d9582acb183ccda82ffe0105b8312a307 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 23 May 2025 17:34:52 +0800 Subject: [PATCH 2/2] wip: save --- .../__tests__/customElement.spec.ts | 43 +++++++++++++++++++ packages/runtime-dom/src/apiCustomElement.ts | 21 ++++++--- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index 09d4b0bdee5..23d5ffed0fa 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -977,6 +977,49 @@ describe('defineCustomElement', () => { assertStyles(el, [`div { color: green; }`, `div { color: blue; }`]) }) + test('inject nested child component styles', async () => { + const Baz = defineComponent({ + styles: [`div { color: yellow; }`], + render() { + return h(Bar) + }, + }) + const Bar = defineComponent({ + styles: [`div { color: green; }`], + render() { + return 'bar' + }, + }) + const WrapperBar = defineComponent({ + styles: [`div { color: blue; }`], + render() { + return h(Baz) + }, + }) + const WBaz = defineComponent({ + styles: [`div { color: black; }`], + render() { + return h(WrapperBar) + }, + }) + const Foo = defineCustomElement({ + styles: [`div { color: red; }`], + render() { + return [h(Baz), h(WBaz)] + }, + }) + customElements.define('my-el-with-inject-nested-child-styles', Foo) + container.innerHTML = `` + const el = container.childNodes[0] as VueElement + assertStyles(el, [ + `div { color: green; }`, + `div { color: yellow; }`, + `div { color: blue; }`, + `div { color: black; }`, + `div { color: red; }`, + ]) + }) + test('with nonce', () => { const Foo = defineCustomElement( { diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 15b3c33d878..5de32735fd5 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -232,7 +232,8 @@ export class VueElement private _styleChildren = new WeakSet() private _pendingResolve: Promise | undefined private _parent: VueElement | undefined - private _styleAnchor?: HTMLStyleElement | Text + private _styleAnchors: WeakMap = + new WeakMap() /** * dev only */ @@ -599,12 +600,13 @@ export class VueElement // to inject child styles before it. if (parentComp && !parentComp.styles) { const anchor = document.createTextNode('') - if (this._styleAnchor) { - this.shadowRoot!.insertBefore(anchor, this._styleAnchor) + const styleAnchor = this._styleAnchors.get(this._def) + if (styleAnchor) { + this.shadowRoot!.insertBefore(anchor, styleAnchor) } else { this.shadowRoot!.prepend(anchor) } - this._styleAnchor = anchor + this._styleAnchors.set(this._def, anchor) } const nonce = this._nonce @@ -616,12 +618,19 @@ export class VueElement // inject styles before parent styles if (parentComp) { - this.shadowRoot!.insertBefore(s, last || this._styleAnchor!) + this.shadowRoot!.insertBefore( + s, + last || + this._styleAnchors.get(parentComp) || + this._styleAnchors.get(this._def) || + null, + ) } else { this.shadowRoot!.prepend(s) - this._styleAnchor = s + this._styleAnchors.set(this._def, s) } last = s + if (owner && i === 0) this._styleAnchors.set(owner, s) // record for HMR if (__DEV__) {