diff --git a/src/component.js b/src/component.js index 4f4e5b2744..b56c1bcb30 100644 --- a/src/component.js +++ b/src/component.js @@ -136,7 +136,12 @@ function renderComponent(component) { newVNode, oldVNode, component._globalContext, - component._parentDom.ownerSVGElement !== undefined, + component._parentDom.namespaceURI == 'http://www.w3.org/2000/svg' + ? 2 + : component._parentDom.namespaceURI == + 'http://www.w3.org/1998/Math/MathML' + ? 3 + : 1, oldVNode._flags & MODE_HYDRATE ? [oldDom] : null, commitQueue, oldDom == null ? getDomSibling(oldVNode) : oldDom, diff --git a/src/diff/children.js b/src/diff/children.js index 1b4d6b55dc..87c9590ac0 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -15,7 +15,7 @@ import { getDomSibling } from '../component'; * diff'ed against newParentVNode * @param {object} globalContext The current context object - modified by * getChildContext - * @param {boolean} isSvg Whether or not this DOM node is an SVG node + * @param {ElementNamespace} namespace Current namespace of the DOM node (HTML, SVG, or MathML) * @param {Array} excessDomChildren * @param {Array} commitQueue List of components which have callbacks * to invoke in commitRoot @@ -32,7 +32,7 @@ export function diffChildren( newParentVNode, oldParentVNode, globalContext, - isSvg, + namespace, excessDomChildren, commitQueue, oldDom, @@ -87,7 +87,7 @@ export function diffChildren( childVNode, oldVNode, globalContext, - isSvg, + namespace, excessDomChildren, commitQueue, oldDom, diff --git a/src/diff/index.js b/src/diff/index.js index 3766beb7d1..fbef9a0aca 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -18,7 +18,7 @@ import options from '../options'; * @param {VNode} oldVNode The old virtual node * @param {object} globalContext The current context object. Modified by * getChildContext - * @param {boolean} isSvg Whether or not this element is an SVG node + * @param {ElementNamespace} namespace Current namespace of the DOM node (HTML, SVG, or MathML) * @param {Array} excessDomChildren * @param {Array} commitQueue List of components which have callbacks * to invoke in commitRoot @@ -34,7 +34,7 @@ export function diff( newVNode, oldVNode, globalContext, - isSvg, + namespace, excessDomChildren, commitQueue, oldDom, @@ -245,7 +245,7 @@ export function diff( newVNode, oldVNode, globalContext, - isSvg, + namespace, excessDomChildren, commitQueue, oldDom, @@ -294,7 +294,7 @@ export function diff( newVNode, oldVNode, globalContext, - isSvg, + namespace, excessDomChildren, commitQueue, isHydrating, @@ -341,7 +341,7 @@ export function commitRoot(commitQueue, root, refQueue) { * @param {VNode} newVNode The new virtual node * @param {VNode} oldVNode The old virtual node * @param {object} globalContext The current context object - * @param {boolean} isSvg Whether or not this DOM node is an SVG node + * @param {ElementNamespace} namespace Current namespace of the DOM node (HTML, SVG, or MathML) * @param {Array} excessDomChildren * @param {Array} commitQueue List of components which have callbacks * to invoke in commitRoot @@ -354,7 +354,7 @@ function diffElementNodes( newVNode, oldVNode, globalContext, - isSvg, + namespace, excessDomChildren, commitQueue, isHydrating, @@ -375,8 +375,9 @@ function diffElementNodes( let inputValue; let checked; - // Tracks entering and exiting SVG namespace when descending through the tree. - if (nodeType === 'svg') isSvg = true; + // Tracks entering and exiting namespaces when descending through the tree. + if (nodeType === 'svg') namespace = 2; + else if (nodeType === 'math') namespace = 3; if (excessDomChildren != null) { for (i = 0; i < excessDomChildren.length; i++) { @@ -402,10 +403,15 @@ function diffElementNodes( return document.createTextNode(newProps); } - if (isSvg) { - dom = document.createElementNS('http://www.w3.org/2000/svg', nodeType); - } else { + if (namespace == 1) { dom = document.createElement(nodeType, newProps.is && newProps); + } else { + dom = document.createElementNS( + namespace == 2 + ? 'http://www.w3.org/2000/svg' + : 'http://www.w3.org/1998/Math/MathML', + nodeType + ); } // we created a new parent, so none of the previously attached children can be reused: @@ -449,7 +455,7 @@ function diffElementNodes( ) { continue; } - setProperty(dom, i, null, value, isSvg); + setProperty(dom, i, null, value, namespace); } } @@ -470,7 +476,7 @@ function diffElementNodes( (!isHydrating || typeof value == 'function') && oldProps[i] !== value ) { - setProperty(dom, i, value, oldProps[i], isSvg); + setProperty(dom, i, value, oldProps[i], namespace); } } @@ -496,7 +502,7 @@ function diffElementNodes( newVNode, oldVNode, globalContext, - isSvg && nodeType !== 'foreignObject', + nodeType === 'foreignObject' ? 1 : namespace, excessDomChildren, commitQueue, excessDomChildren @@ -530,12 +536,12 @@ function diffElementNodes( // again, which triggers IE11 to re-evaluate the select value (nodeType === 'option' && inputValue !== oldProps[i])) ) { - setProperty(dom, i, inputValue, oldProps[i], false); + setProperty(dom, i, inputValue, oldProps[i], namespace); } i = 'checked'; if (checked !== undefined && checked !== dom[i]) { - setProperty(dom, i, checked, oldProps[i], false); + setProperty(dom, i, checked, oldProps[i], namespace); } } } diff --git a/src/diff/props.js b/src/diff/props.js index 722f1a5061..0858d9e77f 100644 --- a/src/diff/props.js +++ b/src/diff/props.js @@ -32,9 +32,9 @@ let eventClock = 0; * @param {string} name The name of the property to set * @param {*} value The value to set the property to * @param {*} oldValue The old value the property had - * @param {boolean} isSvg Whether or not this DOM node is an SVG node or not + * @param {ElementNamespace} namespace Whether or not this DOM node is an SVG node or not */ -export function setProperty(dom, name, value, oldValue, isSvg) { +export function setProperty(dom, name, value, oldValue, namespace) { let useCapture; o: if (name === 'style') { @@ -83,10 +83,10 @@ export function setProperty(dom, name, value, oldValue, isSvg) { if (!oldValue) { value._attached = eventClock; dom.addEventListener( - name, - useCapture ? eventProxyCapture : eventProxy, - useCapture - ); + name, + useCapture ? eventProxyCapture : eventProxy, + useCapture + ); } else { value._attached = oldValue._attached; } @@ -98,7 +98,7 @@ export function setProperty(dom, name, value, oldValue, isSvg) { ); } } else { - if (isSvg) { + if (namespace == 2) { // Normalize incorrect prop usage for SVG: // - xlink:href / xlinkHref --> href (xlink:href was removed from SVG and isn't needed) // - className --> class diff --git a/src/internal.d.ts b/src/internal.d.ts index fda7ce89fd..db760b3ab7 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -16,6 +16,12 @@ declare global { useDebugvalue = 11 } + export enum ElementNamespace { + html = 1, + svg = 2, + mathml = 3 + } + export interface DevSource { fileName: string; lineNumber: number; @@ -83,8 +89,8 @@ declare global { export type ComponentType

= ComponentClass

| FunctionComponent

; export interface PreactElement extends preact.ContainerNode { - // SVG detection - readonly ownerSVGElement?: SVGElement['ownerSVGElement']; + // Namespace detection + readonly namespaceURI?: string; // Property used to update Text nodes data?: CharacterData['data']; // Property to set __dangerouslySetInnerHTML diff --git a/src/render.js b/src/render.js index 1ee326bc92..2861befbee 100644 --- a/src/render.js +++ b/src/render.js @@ -41,7 +41,11 @@ export function render(vnode, parentDom, replaceNode) { vnode, oldVNode || EMPTY_OBJ, EMPTY_OBJ, - parentDom.ownerSVGElement !== undefined, + parentDom.namespaceURI == 'http://www.w3.org/2000/svg' + ? 2 + : parentDom.namespaceURI == 'http://www.w3.org/1998/Math/MathML' + ? 3 + : 1, !isHydrating && replaceNode ? [replaceNode] : oldVNode diff --git a/test/browser/mathml.test.js b/test/browser/mathml.test.js new file mode 100644 index 0000000000..530a85ae55 --- /dev/null +++ b/test/browser/mathml.test.js @@ -0,0 +1,112 @@ +import { createElement, render } from 'preact'; +import { setupScratch, teardown } from '../_util/helpers'; + +/** @jsx createElement */ + +describe('mathml', () => { + let scratch; + + beforeEach(() => { + scratch = setupScratch(); + }); + + afterEach(() => { + teardown(scratch); + }); + + it('should render with the correct namespace URI', () => { + render(, scratch); + + let namespace = scratch.querySelector('math').namespaceURI; + + expect(namespace).to.equal('http://www.w3.org/1998/Math/MathML'); + }); + + it('should render children with the correct namespace URI', () => { + render( + + + , + scratch + ); + + let namespace = scratch.querySelector('mrow').namespaceURI; + + expect(namespace).to.equal('http://www.w3.org/1998/Math/MathML'); + }); + + it('should inherit correct namespace URI from parent', () => { + const math = document.createElementNS( + 'http://www.w3.org/1998/Math/MathML', + 'math' + ); + scratch.appendChild(math); + + render(, scratch.firstChild); + + let namespace = scratch.querySelector('mrow').namespaceURI; + + expect(namespace).to.equal('http://www.w3.org/1998/Math/MathML'); + }); + + it('should use attributes for className', () => { + const Demo = ({ c }) => ( + + + c + 2 + + + ); + render(, scratch); + let root = scratch.firstChild; + sinon.spy(root, 'removeAttribute'); + render(, scratch); + expect(root.removeAttribute).to.have.been.calledOnce.and.calledWith( + 'class' + ); + + root.removeAttribute.restore(); + + render(

, scratch); + render(, scratch); + root = scratch.firstChild; + sinon.spy(root, 'setAttribute'); + render(, scratch); + expect(root.setAttribute).to.have.been.calledOnce.and.calledWith( + 'class', + 'foo_2' + ); + + root.setAttribute.restore(); + }); + + it('should support class attribute', () => { + render(, scratch); + + expect(scratch.innerHTML).to.contain(` class="foo bar"`); + }); + + it('should support className attribute', () => { + render(, scratch); + + expect(scratch.innerHTML).to.contain(` class="foo bar"`); + }); + + it('should transition from DOM to MathML and back', () => { + render( +
+ + + c + 2 + + +
, + scratch + ); + + expect(scratch.firstChild).to.be.an('HTMLDivElement'); + expect(scratch.firstChild.firstChild).to.be.an('MathMLElement'); + }); +}); diff --git a/test/browser/svg.test.js b/test/browser/svg.test.js index 65ddf642d6..420f2d474e 100644 --- a/test/browser/svg.test.js +++ b/test/browser/svg.test.js @@ -133,6 +133,30 @@ describe('svg', () => { expect(namespace).to.equal('http://www.w3.org/2000/svg'); }); + it('should render children with the correct namespace URI', () => { + render( + + + , + scratch + ); + + let namespace = scratch.querySelector('text').namespaceURI; + + expect(namespace).to.equal('http://www.w3.org/2000/svg'); + }); + + it('should inherit correct namespace URI from parent', () => { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + scratch.appendChild(svg); + + render(, scratch.firstChild); + + let namespace = scratch.querySelector('text').namespaceURI; + + expect(namespace).to.equal('http://www.w3.org/2000/svg'); + }); + it('should use attributes for className', () => { const Demo = ({ c }) => (