diff --git a/src/component.js b/src/component.js index 4f4e5b2744..287f3233f4 100644 --- a/src/component.js +++ b/src/component.js @@ -136,7 +136,7 @@ function renderComponent(component) { newVNode, oldVNode, component._globalContext, - component._parentDom.ownerSVGElement !== undefined, + component._parentDom.namespaceURI, 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..912cc72d7c 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 {string} 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..a8bb012672 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 {string} 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 {string} 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,11 @@ 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 = 'http://www.w3.org/2000/svg'; + else if (nodeType === 'math') + namespace = 'http://www.w3.org/1998/Math/MathML'; + else if (!namespace) namespace = 'http://www.w3.org/1999/xhtml'; if (excessDomChildren != null) { for (i = 0; i < excessDomChildren.length; i++) { @@ -402,11 +405,11 @@ function diffElementNodes( return document.createTextNode(newProps); } - if (isSvg) { - dom = document.createElementNS('http://www.w3.org/2000/svg', nodeType); - } else { - dom = document.createElement(nodeType, newProps.is && newProps); - } + dom = document.createElementNS( + namespace, + nodeType, + newProps.is && newProps + ); // we created a new parent, so none of the previously attached children can be reused: excessDomChildren = null; @@ -449,7 +452,7 @@ function diffElementNodes( ) { continue; } - setProperty(dom, i, null, value, isSvg); + setProperty(dom, i, null, value, namespace); } } @@ -470,7 +473,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 +499,9 @@ function diffElementNodes( newVNode, oldVNode, globalContext, - isSvg && nodeType !== 'foreignObject', + nodeType === 'foreignObject' + ? 'http://www.w3.org/1999/xhtml' + : namespace, excessDomChildren, commitQueue, excessDomChildren @@ -530,12 +535,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..d03492293a 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 {string} 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 == 'http://www.w3.org/2000/svg') { // 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..ab03abc67c 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -83,8 +83,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..eeeb452a4f 100644 --- a/src/render.js +++ b/src/render.js @@ -41,7 +41,7 @@ export function render(vnode, parentDom, replaceNode) { vnode, oldVNode || EMPTY_OBJ, EMPTY_OBJ, - parentDom.ownerSVGElement !== undefined, + parentDom.namespaceURI, !isHydrating && replaceNode ? [replaceNode] : oldVNode diff --git a/test/browser/mathml.test.js b/test/browser/mathml.test.js new file mode 100644 index 0000000000..92e2bb6bcf --- /dev/null +++ b/test/browser/mathml.test.js @@ -0,0 +1,95 @@ +import { createElement, Component, render } from 'preact'; +import { setupRerender } from 'preact/test-utils'; +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 inherit correct namespace URI from parent upon updating', () => { + setupRerender(); + + const math = document.createElementNS( + 'http://www.w3.org/1998/Math/MathML', + 'math' + ); + scratch.appendChild(math); + + class App extends Component { + state = { show: true }; + componentDidMount() { + // eslint-disable-next-line + this.setState({ show: false }, () => { + expect(scratch.querySelector('mo').namespaceURI).to.equal( + 'http://www.w3.org/1998/Math/MathML' + ); + }); + } + render() { + return this.state.show ? 1 : 2; + } + } + + render(, scratch.firstChild); + }); + + 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..1b19b50854 100644 --- a/test/browser/svg.test.js +++ b/test/browser/svg.test.js @@ -1,4 +1,5 @@ -import { createElement, render } from 'preact'; +import { createElement, Component, render } from 'preact'; +import { setupRerender } from 'preact/test-utils'; import { setupScratch, teardown, sortAttributes } from '../_util/helpers'; /** @jsx createElement */ @@ -133,6 +134,54 @@ 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 inherit correct namespace URI from parent upon updating', () => { + setupRerender(); + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + scratch.appendChild(svg); + + class App extends Component { + state = { show: true }; + componentDidMount() { + // eslint-disable-next-line + this.setState({ show: false }, () => { + expect(scratch.querySelector('circle').namespaceURI).to.equal( + 'http://www.w3.org/2000/svg' + ); + }); + } + render() { + return this.state.show ? : ; + } + } + + render(, scratch.firstChild); + }); + it('should use attributes for className', () => { const Demo = ({ c }) => (