Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(runtime-core): Fix cssVar cannot work in teleport #7364

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 4 additions & 9 deletions packages/runtime-core/src/components/Teleport.ts
Expand Up @@ -398,14 +398,9 @@ export const Teleport = TeleportImpl as unknown as {

function updateCssVars(vnode: VNode) {
// presence of .ut method indicates owner component uses css vars.
// code path here can assume browser environment.
const ctx = vnode.ctx
if (ctx && ctx.ut) {
let node = (vnode.children as VNode[])[0].el!
while (node !== vnode.targetAnchor) {
if (node.nodeType === 1) node.setAttribute('data-v-owner', ctx.uid)
node = node.nextSibling
}
ctx.ut()
let ctx = vnode.ctx;
while (ctx) {
ctx.ut?.()
ctx = ctx.parent
}
}
155 changes: 154 additions & 1 deletion packages/runtime-dom/__tests__/helpers/useCssVars.spec.ts
Expand Up @@ -9,7 +9,14 @@ import {
ComponentOptions,
Suspense,
Teleport,
FunctionalComponent
FunctionalComponent,
renderSlot,
withCtx,
openBlock,
createBlock,
createElementBlock,
createCommentVNode,
createElementVNode
} from '@vue/runtime-dom'

describe('useCssVars', () => {
Expand Down Expand Up @@ -275,4 +282,150 @@ describe('useCssVars', () => {
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
}
})

test('with teleport(slot in child component)', async () => {
document.body.innerHTML = ''
const state = reactive({ color: 'red' })
const root = document.createElement('div')
const target = document.body
const toggle = ref(true)
const comp = {
render(ctx: any) {
return renderSlot(ctx.$slots, 'default')
}
}
const App = {
setup() {
useCssVars(() => state)
return () => [
h(Teleport, { to: target }, [
h(
comp,
{},
{
default: withCtx(() => [
toggle.value
? (openBlock(),
createElementBlock(
'div',
{
key: 0,
class: 'text'
},
' test '
))
: createCommentVNode('v-if', true)
]),
_: 1
}
)
])
]
}
}

render(h(App), root)
await nextTick()
expect(target.children.length).toBe(1)
for (const c of [].slice.call(target.children as any)) {
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
}

toggle.value = false
await nextTick()
toggle.value = true
await nextTick()
expect(target.children.length).toBe(1)
for (const c of [].slice.call(target.children as any)) {
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
}
})

test('with teleport(as top level in child component)', async () => {
document.body.innerHTML = ''
const state = reactive({ color: 'red' })
const root = document.createElement('div')
const target = document.body
const toggle = ref(false)
const comp = {
render(ctx: any) {
return (
openBlock(),
createBlock(Teleport, { to: 'body' }, [
createElementVNode('div', { class: 'text' }, ' this ', -1)
])
)
}
}
const App = {
setup() {
useCssVars(() => state)
return () => [
toggle.value ? h(comp, {}) : createCommentVNode('v-if', true)
]
}
}

render(h(App), root)
await nextTick()
toggle.value = true
await nextTick()
expect(target.children.length).toBe(1)
for (const c of [].slice.call(target.children as any)) {
expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
}
})

test('with teleport(multilevel nesting)', async () => {
document.body.innerHTML = ''
const state1 = reactive({ color1: 'red' })
const state2 = reactive({ color2: 'green' })

const root = document.createElement('div')
const target = document.body
const toggle = ref(false)

const comp2 = {
render(ctx: any) {
return (
openBlock(),
createBlock(Teleport, { to: 'body' }, [
createElementVNode('div', {}, ' green ', -1)
])
)
}
}

const comp1 = {
setup() {
useCssVars(() => state2)
return ()=>(
openBlock(),
createBlock(Teleport, { to: 'body' }, [
createElementVNode('div', {}, ' red ', -1),
h(comp2,{})
])
)
}
}

const App = {
setup() {
useCssVars(() => state1)
return () => [
toggle.value ? h(comp1, {}) : createCommentVNode('v-if', true)
]
}
}

render(h(App), root)
await nextTick()
toggle.value = true
await nextTick()
expect(target.children.length).toBe(2)
for (const c of [].slice.call(target.children as any)) {
expect((c as HTMLElement).style.getPropertyValue(`--color1`)).toBe('red')
expect((c as HTMLElement).style.getPropertyValue(`--color2`)).toBe('green')
}
})
})
59 changes: 42 additions & 17 deletions packages/runtime-dom/src/helpers/useCssVars.ts
Expand Up @@ -6,9 +6,10 @@ import {
Static,
watchPostEffect,
onMounted,
onUnmounted
onUnmounted,
Teleport
} from '@vue/runtime-core'
import { ShapeFlags } from '@vue/shared'
import { ShapeFlags, isArray } from '@vue/shared'

/**
* Runtime helper for SFC's CSS variable injection feature.
Expand All @@ -17,6 +18,7 @@ import { ShapeFlags } from '@vue/shared'
export function useCssVars(getter: (ctx: any) => Record<string, string>) {
if (!__BROWSER__ && !__TEST__) return

const obs = new Map<Node, MutationObserver>()
const instance = getCurrentInstance()
/* istanbul ignore next */
if (!instance) {
Expand All @@ -25,34 +27,41 @@ export function useCssVars(getter: (ctx: any) => Record<string, string>) {
return
}

const updateTeleports = (instance.ut = (vars = getter(instance.proxy)) => {
Array.from(
document.querySelectorAll(`[data-v-owner="${instance.uid}"]`)
).forEach(node => setVarsOnNode(node, vars))
})
const createObserve = (container: Node) => {
if (obs.has(container)) return
const ob = new MutationObserver(setVars)
ob.observe(container, { childList: true })
obs.set(container, ob)
}

const setVars = () => {
const vars = getter(instance.proxy)
setVarsOnVNode(instance.subTree, vars)
updateTeleports(vars)
setVarsOnVNode(instance.subTree, vars, createObserve)
}

watchPostEffect(setVars)

onMounted(() => {
const ob = new MutationObserver(setVars)
ob.observe(instance.subTree.el!.parentNode, { childList: true })
onUnmounted(() => ob.disconnect())
createObserve(instance.subTree.el!.parentNode)
onUnmounted(() => {
obs.forEach(ob => ob.disconnect())
obs.clear()
})
})
}

function setVarsOnVNode(vnode: VNode, vars: Record<string, string>) {
function setVarsOnVNode(
vnode: VNode,
vars: Record<string, string>,
createObserve: (container: Node) => void,
flag?: Boolean
) {
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
const suspense = vnode.suspense!
vnode = suspense.activeBranch!
if (suspense.pendingBranch && !suspense.isHydrating) {
suspense.effects.push(() => {
setVarsOnVNode(suspense.activeBranch!, vars)
setVarsOnVNode(suspense.activeBranch!, vars, createObserve, flag)
})
}
}
Expand All @@ -62,10 +71,26 @@ function setVarsOnVNode(vnode: VNode, vars: Record<string, string>) {
vnode = vnode.component.subTree
}

if (vnode.shapeFlag & ShapeFlags.ELEMENT && vnode.el) {
setVarsOnNode(vnode.el as Node, vars)
if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
if (!flag && vnode.el) {
setVarsOnNode(vnode.el as Node, vars)
}
if (isArray(vnode.children)) {
;(vnode.children as VNode[]).forEach(c =>
setVarsOnVNode(c, vars, createObserve, true)
)
}
} else if (vnode.type === Fragment) {
;(vnode.children as VNode[]).forEach(c => setVarsOnVNode(c, vars))
;(vnode.children as VNode[]).forEach(c =>
setVarsOnVNode(c, vars, createObserve, flag)
)
} else if (vnode.type === Teleport) {
if (vnode.target) {
;(vnode.children as VNode[]).forEach(c =>
setVarsOnVNode(c, vars, createObserve)
)
createObserve(vnode.target as Node)
}
} else if (vnode.type === Static) {
let { el, anchor } = vnode
while (el) {
Expand Down