Skip to content

Commit

Permalink
fix: create svg nested children with correct namespace
Browse files Browse the repository at this point in the history
  • Loading branch information
Varixo committed Feb 7, 2025
1 parent cb8013c commit a7749a3
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/long-shirts-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/core': patch
---

fix: create svg nested children with correct namespace
4 changes: 3 additions & 1 deletion packages/qwik/src/core/client/vnode-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,9 @@ export const vnode_diff = (

function expectElement(jsx: JSXNodeInternal, elementName: string) {
const isSameElementName =
vCurrent && vnode_isElementVNode(vCurrent) && elementName === vnode_getElementName(vCurrent);
vCurrent &&
vnode_isElementVNode(vCurrent) &&
elementName.toLowerCase() === vnode_getElementName(vCurrent);
const jsxKey: string | null = jsx.key;
let needsQDispatchEventPatch = false;
const currentFile = getFileLocationFromJsx(jsx.dev);
Expand Down
17 changes: 10 additions & 7 deletions packages/qwik/src/core/client/vnode-namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from './types';
import {
ensureElementVNode,
fastNamespaceURI,
shouldIgnoreChildren,
vnode_getDOMChildNodes,
vnode_getDomParentVNode,
Expand All @@ -35,13 +36,15 @@ export const vnode_isDefaultNamespace = (vnode: ElementVNode): boolean => {
return (flags & VNodeFlags.NAMESPACE_MASK) === 0;
};

export const vnode_getElementNamespaceFlags = (elementName: string) => {
if (isSvgElement(elementName)) {
return VNodeFlags.NS_svg;
} else if (isMathElement(elementName)) {
return VNodeFlags.NS_math;
} else {
return VNodeFlags.NS_html;
export const vnode_getElementNamespaceFlags = (element: Element) => {
const namespace = fastNamespaceURI(element);
switch (namespace) {
case SVG_NS:
return VNodeFlags.NS_svg;
case MATH_NS:
return VNodeFlags.NS_math;
default:
return VNodeFlags.NS_html;
}
};

Expand Down
23 changes: 20 additions & 3 deletions packages/qwik/src/core/client/vnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1192,9 +1192,10 @@ export const vnode_getElementName = (vnode: ElementVNode): string => {
const elementVNode = ensureElementVNode(vnode);
let elementName = elementVNode[ElementVNodeProps.elementName];
if (elementName === undefined) {
elementName = elementVNode[ElementVNodeProps.elementName] =
elementVNode[ElementVNodeProps.element].nodeName.toLowerCase();
elementVNode[VNodeProps.flags] |= vnode_getElementNamespaceFlags(elementName);
const element = elementVNode[ElementVNodeProps.element];
const nodeName = fastNodeName(element)!.toLowerCase();
elementName = elementVNode[ElementVNodeProps.elementName] = nodeName;
elementVNode[VNodeProps.flags] |= vnode_getElementNamespaceFlags(element);
}
return elementName;
};
Expand Down Expand Up @@ -1405,6 +1406,22 @@ const fastFirstChild = (node: Node | null): Node | null => {
return node;
};

let _fastNamespaceURI: ((this: Element) => string | null) | null = null;
export const fastNamespaceURI = (element: Element): string | null => {
if (!_fastNamespaceURI) {
_fastNamespaceURI = fastGetter<typeof _fastNamespaceURI>(element, 'namespaceURI')!;
}
return _fastNamespaceURI.call(element);
};

let _fastNodeName: ((this: Element) => string | null) | null = null;
export const fastNodeName = (element: Element): string | null => {
if (!_fastNodeName) {
_fastNodeName = fastGetter<typeof _fastNodeName>(element, 'nodeName')!;
}
return _fastNodeName.call(element);
};

const fastGetter = <T>(prototype: any, name: string): T => {
let getter: any;
while (prototype && !(getter = Object.getOwnPropertyDescriptor(prototype, name)?.get)) {
Expand Down
60 changes: 60 additions & 0 deletions packages/qwik/src/core/tests/render-namespace.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,66 @@ describe.each([

await expect(container.document.body.querySelector('button')).toMatchDOM(<button></button>);
});

it('should rerender svg nested children', async () => {
const SvgComp = component$((props: { show: boolean }) => {
return (
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" key="0">
<defs>
{props.show && (
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#ff0000;stop-opacity:1px" />
<stop offset="100%" style="stop-color:#0000ff;stop-opacity:1px" />
</linearGradient>
)}
</defs>
</svg>
);
});
const Parent = component$(() => {
const show = useSignal(false);
return (
<button onClick$={() => (show.value = !show.value)}>
<SvgComp show={show.value} />
</button>
);
});
const { vNode, container } = await render(<Parent />, { debug });
expect(vNode).toMatchVDOM(
<Component>
<button>
<Component>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" key="0">
<defs></defs>
</svg>
</Component>
</button>
</Component>
);

await trigger(container.element, 'button', 'click');
expect(vNode).toMatchVDOM(
<Component>
<button>
<Component>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" key="0">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#ff0000;stop-opacity:1px" />
<stop offset="100%" style="stop-color:#0000ff;stop-opacity:1px" />
</linearGradient>
</defs>
</svg>
</Component>
</button>
</Component>
);

expect(
container.document.querySelector('svg')?.querySelector('linearGradient')?.namespaceURI
).toEqual(SVG_NS);
});

it('should rerender svg child elements', async () => {
const SvgComp = component$((props: { child: JSXOutput }) => {
return (
Expand Down

0 comments on commit a7749a3

Please sign in to comment.