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..23d5ffed0fa 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -916,6 +916,110 @@ 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('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 56b86a5fd9e..5de32735fd5 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -232,6 +232,8 @@ export class VueElement private _styleChildren = new WeakSet() private _pendingResolve: Promise | undefined private _parent: VueElement | undefined + private _styleAnchors: WeakMap = + new WeakMap() /** * dev only */ @@ -584,6 +586,7 @@ export class VueElement private _applyStyles( styles: string[] | undefined, owner?: ConcreteComponent, + parentComp?: ConcreteComponent & CustomElementOptions, ) { if (!styles) return if (owner) { @@ -592,12 +595,43 @@ 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('') + const styleAnchor = this._styleAnchors.get(this._def) + if (styleAnchor) { + this.shadowRoot!.insertBefore(anchor, styleAnchor) + } else { + this.shadowRoot!.prepend(anchor) + } + this._styleAnchors.set(this._def, 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._styleAnchors.get(parentComp) || + this._styleAnchors.get(this._def) || + null, + ) + } else { + this.shadowRoot!.prepend(s) + this._styleAnchors.set(this._def, s) + } + last = s + if (owner && i === 0) this._styleAnchors.set(owner, s) + // record for HMR if (__DEV__) { if (owner) { @@ -665,8 +699,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) } /**