Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions packages/runtime-core/__tests__/hydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,56 @@ describe('SSR hydration', () => {
)
})

test('SVG element coordinate attributes are set as attributes not properties', () => {
const { container } = mountWithHydration(
'<svg width="200" height="200"><line x1="10" y1="20" x2="180" y2="190" /></svg>',
() =>
h('svg', { width: 200, height: 200 }, [
h('line', { x1: 10, y1: 20, x2: 180, y2: 190 }),
]),
)
const line = container.querySelector('line')!
expect(line.getAttribute('x1')).toBe('10')
expect(line.getAttribute('y1')).toBe('20')
expect(line.getAttribute('x2')).toBe('180')
expect(line.getAttribute('y2')).toBe('190')
// Should not warn about setting SVG coordinate props
expect(`Failed setting prop`).not.toHaveBeenWarned()
})

test('SVG element circle coordinate attributes hydrate correctly', () => {
const { container } = mountWithHydration(
'<svg width="200" height="200"><circle cx="100" cy="100" r="50" /></svg>',
() =>
h('svg', { width: 200, height: 200 }, [
h('circle', { cx: 100, cy: 100, r: 50 }),
]),
)
const circle = container.querySelector('circle')!
expect(circle.getAttribute('cx')).toBe('100')
expect(circle.getAttribute('cy')).toBe('100')
expect(circle.getAttribute('r')).toBe('50')
expect(`Failed setting prop`).not.toHaveBeenWarned()
})

test('SVG foreignObject own attributes hydrate correctly through HTML path', () => {
const { container } = mountWithHydration(
'<svg width="200" height="200"><foreignObject x="10" y="20" width="100" height="50" /></svg>',
() =>
h('svg', { width: 200, height: 200 }, [
h('foreignObject', { x: 10, y: 20, width: 100, height: 50 }),
]),
)
const fo = container.querySelector('foreignObject')!
// foreignObject should exclude its own attributes from SVG namespace handling
// so they fall through to setAttribute via the non-SVG prop path
expect(fo.getAttribute('x')).toBe('10')
expect(fo.getAttribute('y')).toBe('20')
expect(fo.getAttribute('width')).toBe('100')
expect(fo.getAttribute('height')).toBe('50')
expect(`Failed setting prop`).not.toHaveBeenWarned()
})

test('force hydrate prop with `.prop` modifier', () => {
const { container } = mountWithHydration('<input type="checkbox">', () =>
h('input', {
Expand Down
15 changes: 13 additions & 2 deletions packages/runtime-core/src/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,17 @@ export function createHydrationFunctions(
}
}

// determine SVG/MathML namespace from the element itself
// so that patchProp can properly handle SVG coordinate attributes
// (x1, y1, cx, cy, r, etc.) which must be set as attributes not DOM props
// computed after template replacement so that el is the actual element,
// not a <template> placeholder (relevant for <Transition appear>)
const elementNamespace =
el.namespaceURI!.includes('svg') && el.tagName !== 'foreignObject'
? ('svg' as const)
: el.namespaceURI!.includes('MathML')
? ('mathml' as const)
: undefined
// props
if (props) {
if (
Expand Down Expand Up @@ -520,7 +531,7 @@ export function createHydrationFunctions(
(isCustomElement && !isReservedProp(key)) ||
(dynamicProps && dynamicProps.includes(key))
) {
patchProp(el, key, null, props[key], undefined, parentComponent)
patchProp(el, key, null, props[key], elementNamespace, parentComponent)
}
}
} else if (props.onClick) {
Expand All @@ -531,7 +542,7 @@ export function createHydrationFunctions(
'onClick',
null,
props.onClick,
undefined,
elementNamespace,
parentComponent,
)
} else if (patchFlag & PatchFlags.STYLE && isReactive(props.style)) {
Expand Down