]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(custom-element): inject child style before parent component's style
authordaiwei <daiwei521@126.com>
Fri, 23 May 2025 03:32:00 +0000 (11:32 +0800)
committerdaiwei <daiwei521@126.com>
Fri, 23 May 2025 08:39:35 +0000 (16:39 +0800)
packages/runtime-core/src/component.ts
packages/runtime-core/src/renderer.ts
packages/runtime-dom/__tests__/customElement.spec.ts
packages/runtime-dom/src/apiCustomElement.ts

index f191c36df1242e8cc505471ed125f73f4172d94c..2c73e0fa3086a12de1a695fa43efd5b37d5d5ff0 100644 (file)
@@ -1256,7 +1256,7 @@ export interface ComponentCustomElementInterface {
   /**
    * @internal
    */
-  _injectChildStyle(type: ConcreteComponent): void
+  _injectChildStyle(type: ConcreteComponent, parent?: ConcreteComponent): void
   /**
    * @internal
    */
index 7b39aa917a25f7bb26ea98c349d61c4c4265aaac..6e31cb01013599bf1a4bd5bc559d633cb2f0c5e8 100644 (file)
@@ -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__) {
index eee2151716e59d87e052a357f01e3182da9d115c..09d4b0bdee51e21bcc9bf5889ca134a4f15329ee 100644 (file)
@@ -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 = `<my-el-with-wrapper-child-styles></my-el-with-wrapper-child-styles>`
+      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 = `<my-el-with-inject-child-styles></my-el-with-inject-child-styles>`
+      const el = container.childNodes[0] as VueElement
+
+      assertStyles(el, [`div { color: green; }`, `div { color: blue; }`])
+    })
+
     test('with nonce', () => {
       const Foo = defineCustomElement(
         {
index 56b86a5fd9e0cea6803cbbafc8050c06f1c933c9..15b3c33d8785d2bd602b128d65f792f4c04b2091 100644 (file)
@@ -232,6 +232,7 @@ export class VueElement
   private _styleChildren = new WeakSet()
   private _pendingResolve: Promise<void> | 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)
   }
 
   /**