Skip to content

Commit 1398bf8

Browse files
authored
fix(custom-element): ensure child component styles are injected in correct order before parent styles (#13374)
close #13029
1 parent 0d63202 commit 1398bf8

File tree

4 files changed

+237
-5
lines changed

4 files changed

+237
-5
lines changed

packages/runtime-core/src/component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1271,7 +1271,7 @@ export interface ComponentCustomElementInterface {
12711271
/**
12721272
* @internal
12731273
*/
1274-
_injectChildStyle(type: ConcreteComponent): void
1274+
_injectChildStyle(type: ConcreteComponent, parent?: ConcreteComponent): void
12751275
/**
12761276
* @internal
12771277
*/

packages/runtime-core/src/renderer.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1367,7 +1367,10 @@ function baseCreateRenderer(
13671367
} else {
13681368
// custom element style injection
13691369
if (root.ce && root.ce._hasShadowRoot()) {
1370-
root.ce._injectChildStyle(type)
1370+
root.ce._injectChildStyle(
1371+
type,
1372+
instance.parent ? instance.parent.type : undefined,
1373+
)
13711374
}
13721375

13731376
if (__DEV__) {

packages/runtime-dom/__tests__/customElement.spec.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,190 @@ describe('defineCustomElement', () => {
12181218
assertStyles(el, [`div { color: blue; }`, `div { color: red; }`])
12191219
})
12201220

1221+
test('root custom element HMR should preserve child-first style order', async () => {
1222+
const Child = defineComponent({
1223+
styles: [`div { color: green; }`],
1224+
render() {
1225+
return 'child'
1226+
},
1227+
})
1228+
const def = defineComponent({
1229+
__hmrId: 'root-child-style-order',
1230+
styles: [`div { color: red; }`],
1231+
render() {
1232+
return h(Child)
1233+
},
1234+
})
1235+
const Foo = defineCustomElement(def)
1236+
customElements.define('my-el-root-hmr-style-order', Foo)
1237+
container.innerHTML = `<my-el-root-hmr-style-order></my-el-root-hmr-style-order>`
1238+
const el = container.childNodes[0] as VueElement
1239+
1240+
assertStyles(el, [`div { color: green; }`, `div { color: red; }`])
1241+
1242+
__VUE_HMR_RUNTIME__.reload(def.__hmrId!, {
1243+
...def,
1244+
styles: [`div { color: blue; }`, `div { color: yellow; }`],
1245+
} as any)
1246+
1247+
await nextTick()
1248+
assertStyles(el, [
1249+
`div { color: green; }`,
1250+
`div { color: blue; }`,
1251+
`div { color: yellow; }`,
1252+
])
1253+
})
1254+
1255+
test('inject child component styles before parent styles', async () => {
1256+
const Baz = () => h(Bar)
1257+
const Bar = defineComponent({
1258+
styles: [`div { color: green; }`],
1259+
render() {
1260+
return 'bar'
1261+
},
1262+
})
1263+
const WrapperBar = defineComponent({
1264+
styles: [`div { color: blue; }`],
1265+
render() {
1266+
return h(Baz)
1267+
},
1268+
})
1269+
const WBaz = () => h(WrapperBar)
1270+
const Foo = defineCustomElement({
1271+
styles: [`div { color: red; }`],
1272+
render() {
1273+
return [h(Baz), h(WBaz)]
1274+
},
1275+
})
1276+
customElements.define('my-el-with-wrapper-child-styles', Foo)
1277+
container.innerHTML = `<my-el-with-wrapper-child-styles></my-el-with-wrapper-child-styles>`
1278+
const el = container.childNodes[0] as VueElement
1279+
1280+
// inject order should be child -> parent
1281+
assertStyles(el, [
1282+
`div { color: green; }`,
1283+
`div { color: blue; }`,
1284+
`div { color: red; }`,
1285+
])
1286+
})
1287+
1288+
test('inject nested child component styles after HMR removes parent styles', async () => {
1289+
const Bar = defineComponent({
1290+
__hmrId: 'nested-child-style-hmr-bar',
1291+
styles: [`div { color: green; }`],
1292+
render() {
1293+
return 'bar'
1294+
},
1295+
})
1296+
const WrapperBar = defineComponent({
1297+
__hmrId: 'nested-child-style-hmr-wrapper',
1298+
styles: [`div { color: blue; }`],
1299+
render() {
1300+
return h(Bar)
1301+
},
1302+
})
1303+
const Foo = defineCustomElement({
1304+
styles: [`div { color: red; }`],
1305+
render() {
1306+
return h(WrapperBar)
1307+
},
1308+
})
1309+
customElements.define('my-el-with-hmr-nested-child-styles', Foo)
1310+
container.innerHTML = `<my-el-with-hmr-nested-child-styles></my-el-with-hmr-nested-child-styles>`
1311+
const el = container.childNodes[0] as VueElement
1312+
1313+
assertStyles(el, [
1314+
`div { color: green; }`,
1315+
`div { color: blue; }`,
1316+
`div { color: red; }`,
1317+
])
1318+
1319+
__VUE_HMR_RUNTIME__.reload(WrapperBar.__hmrId!, {
1320+
...WrapperBar,
1321+
styles: undefined,
1322+
} as any)
1323+
await nextTick()
1324+
assertStyles(el, [`div { color: green; }`, `div { color: red; }`])
1325+
1326+
__VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
1327+
...Bar,
1328+
styles: [`div { color: yellow; }`],
1329+
} as any)
1330+
await nextTick()
1331+
assertStyles(el, [`div { color: yellow; }`, `div { color: red; }`])
1332+
})
1333+
1334+
test('inject child component styles when parent has no styles', async () => {
1335+
const Baz = () => h(Bar)
1336+
const Bar = defineComponent({
1337+
styles: [`div { color: green; }`],
1338+
render() {
1339+
return 'bar'
1340+
},
1341+
})
1342+
const WrapperBar = defineComponent({
1343+
styles: [`div { color: blue; }`],
1344+
render() {
1345+
return h(Baz)
1346+
},
1347+
})
1348+
const WBaz = () => h(WrapperBar)
1349+
// without styles
1350+
const Foo = defineCustomElement({
1351+
render() {
1352+
return [h(Baz), h(WBaz)]
1353+
},
1354+
})
1355+
customElements.define('my-el-with-inject-child-styles', Foo)
1356+
container.innerHTML = `<my-el-with-inject-child-styles></my-el-with-inject-child-styles>`
1357+
const el = container.childNodes[0] as VueElement
1358+
1359+
assertStyles(el, [`div { color: green; }`, `div { color: blue; }`])
1360+
})
1361+
1362+
test('inject nested child component styles', async () => {
1363+
const Baz = defineComponent({
1364+
styles: [`div { color: yellow; }`],
1365+
render() {
1366+
return h(Bar)
1367+
},
1368+
})
1369+
const Bar = defineComponent({
1370+
styles: [`div { color: green; }`],
1371+
render() {
1372+
return 'bar'
1373+
},
1374+
})
1375+
const WrapperBar = defineComponent({
1376+
styles: [`div { color: blue; }`],
1377+
render() {
1378+
return h(Baz)
1379+
},
1380+
})
1381+
const WBaz = defineComponent({
1382+
styles: [`div { color: black; }`],
1383+
render() {
1384+
return h(WrapperBar)
1385+
},
1386+
})
1387+
const Foo = defineCustomElement({
1388+
styles: [`div { color: red; }`],
1389+
render() {
1390+
return [h(Baz), h(WBaz)]
1391+
},
1392+
})
1393+
customElements.define('my-el-with-inject-nested-child-styles', Foo)
1394+
container.innerHTML = `<my-el-with-inject-nested-child-styles></my-el-with-inject-nested-child-styles>`
1395+
const el = container.childNodes[0] as VueElement
1396+
assertStyles(el, [
1397+
`div { color: green; }`,
1398+
`div { color: yellow; }`,
1399+
`div { color: blue; }`,
1400+
`div { color: black; }`,
1401+
`div { color: red; }`,
1402+
])
1403+
})
1404+
12211405
test("child components should not inject styles to root element's shadow root w/ shadowRoot false", async () => {
12221406
const Bar = defineComponent({
12231407
styles: [`div { color: green; }`],

packages/runtime-dom/src/apiCustomElement.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ export class VueElement
235235
private _styleChildren = new WeakSet()
236236
private _pendingResolve: Promise<void> | undefined
237237
private _parent: VueElement | undefined
238+
private _styleAnchors: WeakMap<ConcreteComponent, HTMLStyleElement> =
239+
new WeakMap()
238240
/**
239241
* dev only
240242
*/
@@ -571,6 +573,7 @@ export class VueElement
571573
this._styles.forEach(s => this._root.removeChild(s))
572574
this._styles.length = 0
573575
}
576+
this._styleAnchors.delete(this._def)
574577
this._applyStyles(newStyles)
575578
this._instance = null
576579
this._update()
@@ -607,6 +610,7 @@ export class VueElement
607610
private _applyStyles(
608611
styles: string[] | undefined,
609612
owner?: ConcreteComponent,
613+
parentComp?: ConcreteComponent,
610614
) {
611615
if (!styles) return
612616
if (owner) {
@@ -615,12 +619,25 @@ export class VueElement
615619
}
616620
this._styleChildren.add(owner)
617621
}
622+
618623
const nonce = this._nonce
624+
const root = this.shadowRoot!
625+
const insertionAnchor = parentComp
626+
? this._getStyleAnchor(parentComp) || this._getStyleAnchor(this._def)
627+
: this._getRootStyleInsertionAnchor(root)
628+
let last: HTMLStyleElement | null = null
619629
for (let i = styles.length - 1; i >= 0; i--) {
620630
const s = document.createElement('style')
621631
if (nonce) s.setAttribute('nonce', nonce)
622632
s.textContent = styles[i]
623-
this.shadowRoot!.prepend(s)
633+
634+
root.insertBefore(s, last || insertionAnchor)
635+
last = s
636+
if (i === 0) {
637+
if (!parentComp) this._styleAnchors.set(this._def, s)
638+
if (owner) this._styleAnchors.set(owner, s)
639+
}
640+
624641
// record for HMR
625642
if (__DEV__) {
626643
if (owner) {
@@ -639,6 +656,30 @@ export class VueElement
639656
}
640657
}
641658

659+
private _getStyleAnchor(comp?: ConcreteComponent): HTMLStyleElement | null {
660+
if (!comp) {
661+
return null
662+
}
663+
const anchor = this._styleAnchors.get(comp)
664+
if (anchor && anchor.parentNode === this.shadowRoot) {
665+
return anchor
666+
}
667+
if (anchor) {
668+
this._styleAnchors.delete(comp)
669+
}
670+
return null
671+
}
672+
673+
private _getRootStyleInsertionAnchor(root: ShadowRoot): ChildNode | null {
674+
for (let i = 0; i < root.childNodes.length; i++) {
675+
const node = root.childNodes[i]
676+
if (!(node instanceof HTMLStyleElement)) {
677+
return node
678+
}
679+
}
680+
return null
681+
}
682+
642683
/**
643684
* Only called when shadowRoot is false
644685
*/
@@ -708,8 +749,11 @@ export class VueElement
708749
/**
709750
* @internal
710751
*/
711-
_injectChildStyle(comp: ConcreteComponent & CustomElementOptions): void {
712-
this._applyStyles(comp.styles, comp)
752+
_injectChildStyle(
753+
comp: ConcreteComponent & CustomElementOptions,
754+
parentComp?: ConcreteComponent,
755+
): void {
756+
this._applyStyles(comp.styles, comp, parentComp)
713757
}
714758

715759
/**
@@ -743,6 +787,7 @@ export class VueElement
743787
_removeChildStyle(comp: ConcreteComponent): void {
744788
if (__DEV__) {
745789
this._styleChildren.delete(comp)
790+
this._styleAnchors.delete(comp)
746791
if (this._childStyles && comp.__hmrId) {
747792
// clear old styles
748793
const oldStyles = this._childStyles.get(comp.__hmrId)

0 commit comments

Comments
 (0)