Skip to content

Commit cc18126

Browse files
committed
fix: render SVG attributes with correct namespace
1 parent 9849dcf commit cc18126

File tree

5 files changed

+92
-3
lines changed

5 files changed

+92
-3
lines changed

.changeset/open-beds-grab.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: render SVG attributes with correct namespace

packages/qwik/src/core/client/vnode-diff.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ import {
9191
} from './vnode';
9292
import { mapApp_findIndx } from './util-mapArray';
9393
import { mapArray_set } from './util-mapArray';
94-
import { getNewElementNamespaceData } from './vnode-namespace';
94+
import { getAttributeNamespace, getNewElementNamespaceData } from './vnode-namespace';
9595
import { isSignal } from '../reactive-primitives/utils';
9696
import type { Signal } from '../reactive-primitives/signal.public';
9797
import { executeComponent } from '../shared/component-execution';
@@ -673,6 +673,14 @@ export const vnode_diff = (
673673

674674
value = serializeAttribute(key, value, scopedStyleIdPrefix);
675675
if (value != null) {
676+
if (vNewNode![VNodeProps.flags] & VNodeFlags.NS_svg) {
677+
// only svg elements can have namespace attributes
678+
const namespace = getAttributeNamespace(key);
679+
if (namespace) {
680+
element.setAttributeNS(namespace, key, String(value));
681+
continue;
682+
}
683+
}
676684
element.setAttribute(key, String(value));
677685
}
678686
}

packages/qwik/src/core/client/vnode-namespace.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { isDev } from '@qwik.dev/core/build';
2-
import { HTML_NS, MATH_NS, Q_PROPS_SEPARATOR, SVG_NS } from '../shared/utils/markers';
2+
import {
3+
HTML_NS,
4+
MATH_NS,
5+
Q_PROPS_SEPARATOR,
6+
SVG_NS,
7+
XLINK_NS,
8+
XML_NS,
9+
} from '../shared/utils/markers';
310
import { getDomContainerFromQContainerElement } from './dom-container';
411
import {
512
ElementVNodeProps,
@@ -293,3 +300,22 @@ interface NewElementNamespaceData {
293300
elementNamespace: string;
294301
elementNamespaceFlag: number;
295302
}
303+
304+
export function getAttributeNamespace(attributeName: string): string | null {
305+
switch (attributeName) {
306+
case 'xlink:href':
307+
case 'xlink:actuate':
308+
case 'xlink:arcrole':
309+
case 'xlink:role':
310+
case 'xlink:show':
311+
case 'xlink:title':
312+
case 'xlink:type':
313+
return XLINK_NS;
314+
case 'xml:base':
315+
case 'xml:lang':
316+
case 'xml:space':
317+
return XML_NS;
318+
default:
319+
return null;
320+
}
321+
}

packages/qwik/src/core/shared/utils/markers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,15 @@ export const QContainerSelector =
5151
QContainerValue.TEXT +
5252
'])';
5353

54+
// Node namespaces
5455
export const HTML_NS = 'http://www.w3.org/1999/xhtml';
5556
export const SVG_NS = 'http://www.w3.org/2000/svg';
5657
export const MATH_NS = 'http://www.w3.org/1998/Math/MathML';
5758

59+
// Attributes namespaces
60+
export const XLINK_NS = 'http://www.w3.org/1999/xlink';
61+
export const XML_NS = 'http://www.w3.org/XML/1998/namespace';
62+
5863
export const ResourceEvent = 'qResource';
5964
export const RenderEvent = 'qRender';
6065
export const TaskEvent = 'qTask';

packages/qwik/src/core/tests/render-namespace.spec.tsx

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ import {
77
Fragment,
88
type JSXOutput,
99
} from '@qwik.dev/core';
10-
import { HTML_NS, MATH_NS, QContainerAttr, SVG_NS } from '../shared/utils/markers';
10+
import {
11+
HTML_NS,
12+
MATH_NS,
13+
QContainerAttr,
14+
SVG_NS,
15+
XLINK_NS,
16+
XML_NS,
17+
} from '../shared/utils/markers';
1118
import { QContainerValue } from '../shared/types';
1219

1320
const debug = false; //true;
@@ -346,6 +353,44 @@ describe.each([
346353
</svg>
347354
);
348355
});
356+
357+
describe('xlink and xml namespaces', () => {
358+
it('should render xlink:href and xml:lang', async () => {
359+
const SvgComp = component$(() => {
360+
return (
361+
<svg xmlns="http://www.w3.org/2000/svg" xlink:href="http://www.w3.org/1999/xlink">
362+
<g>
363+
<mask id="logo-d" fill="#fff">
364+
<use xlink:href="#logo-c"></use>
365+
</mask>
366+
</g>
367+
<text xml:lang="en-US">This is some English text</text>
368+
</svg>
369+
);
370+
});
371+
const { vNode, document } = await render(<SvgComp />, { debug });
372+
expect(vNode).toMatchVDOM(
373+
<Component>
374+
<svg xmlns="http://www.w3.org/2000/svg" xlink:href="http://www.w3.org/1999/xlink">
375+
<g>
376+
<mask id="logo-d" fill="#fff">
377+
<use xlink:href="#logo-c"></use>
378+
</mask>
379+
</g>
380+
<text xml:lang="en-US">This is some English text</text>
381+
</svg>
382+
</Component>
383+
);
384+
385+
const useElement = document.querySelector('use');
386+
const xLinkHref = useElement?.attributes.getNamedItem('xlink:href');
387+
expect(xLinkHref?.namespaceURI).toEqual(XLINK_NS);
388+
389+
const textElement = document.querySelector('text');
390+
const xmlLang = textElement?.attributes.getNamedItem('xml:lang');
391+
expect(xmlLang?.namespaceURI).toEqual(XML_NS);
392+
});
393+
});
349394
});
350395

351396
describe('math', () => {

0 commit comments

Comments
 (0)