Merge pull request #5714 from gaearon/passthrough-svg-attributes

Pass all SVG attributes through
This commit is contained in:
Dan Abramov
2015-12-25 13:53:33 +00:00
8 changed files with 332 additions and 38 deletions
@@ -21,6 +21,45 @@ describe('ReactDOMSVG', function() {
ReactDOMServer = require('ReactDOMServer');
});
it('creates initial markup for known hyphenated attributes', function() {
var markup = ReactDOMServer.renderToString(
<svg clip-path="url(#starlet)" />
);
expect(markup).toContain('clip-path="url(#starlet)"');
});
it('creates initial markup for camel case attributes', function() {
var markup = ReactDOMServer.renderToString(
<svg viewBox="0 0 100 100" />
);
expect(markup).toContain('viewBox="0 0 100 100"');
});
it('deprecates camel casing of hyphenated attributes', function() {
spyOn(console, 'error');
var markup = ReactDOMServer.renderToString(
<svg clipPath="url(#starlet)" />
);
expect(markup).toContain('clip-path="url(#starlet)"');
expect(console.error.argsForCall.length).toBe(1);
expect(console.error.argsForCall[0][0]).toContain('clipPath');
expect(console.error.argsForCall[0][0]).toContain('clip-path');
});
it('creates initial markup for unknown hyphenated attributes', function() {
var markup = ReactDOMServer.renderToString(
<svg the-word="the-bird" />
);
expect(markup).toContain('the-word="the-bird"');
});
it('creates initial markup for unknown camel case attributes', function() {
var markup = ReactDOMServer.renderToString(
<svg theWord="theBird" />
);
expect(markup).toContain('theWord="theBird"');
});
it('creates initial namespaced markup', function() {
var markup = ReactDOMServer.renderToString(
<svg>
@@ -124,6 +124,31 @@ var DOMPropertyOperations = {
return name + '=' + quoteAttributeValueForBrowser(value);
},
/**
* Creates markup for an SVG property.
*
* @param {string} name
* @param {*} value
* @return {string} Markup string, or empty string if the property was invalid.
*/
createMarkupForSVGAttribute: function(name, value) {
if (__DEV__) {
ReactDOMInstrumentation.debugTool.onCreateMarkupForSVGAttribute(name, value);
}
if (!isAttributeNameSafe(name) || value == null) {
return '';
}
var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ?
DOMProperty.properties[name] : null;
if (propertyInfo) {
// Migration path for deprecated camelCase aliases for SVG attributes
var { attributeName } = propertyInfo;
return attributeName + '=' + quoteAttributeValueForBrowser(value);
} else {
return name + '=' + quoteAttributeValueForBrowser(value);
}
},
/**
* Sets the value for a property on a node.
*
@@ -183,6 +208,18 @@ var DOMPropertyOperations = {
}
},
setValueForSVGAttribute: function(node, name, value) {
if (__DEV__) {
ReactDOMInstrumentation.debugTool.onSetValueForSVGAttribute(node, name, value);
}
if (DOMProperty.properties.hasOwnProperty(name)) {
// Migration path for deprecated camelCase aliases for SVG attributes
DOMPropertyOperations.setValueForProperty(node, name, value);
} else {
DOMPropertyOperations.setValueForAttribute(node, name, value);
}
},
/**
* Deletes the value for a property on a node.
*
@@ -217,6 +254,16 @@ var DOMPropertyOperations = {
}
},
deleteValueForSVGAttribute: function(node, name) {
var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ?
DOMProperty.properties[name] : null;
if (propertyInfo) {
DOMPropertyOperations.deleteValueForProperty(node, name);
} else {
node.removeAttribute(name);
}
},
};
ReactPerf.measureMethods(DOMPropertyOperations, 'DOMPropertyOperations', {
@@ -647,6 +647,8 @@ ReactDOMComponent.Mixin = {
if (propKey !== CHILDREN) {
markup = DOMPropertyOperations.createMarkupForCustomAttribute(propKey, propValue);
}
} else if (this._namespaceURI === DOMNamespaces.svg) {
markup = DOMPropertyOperations.createMarkupForSVGAttribute(propKey, propValue);
} else {
markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
}
@@ -858,6 +860,11 @@ ReactDOMComponent.Mixin = {
// listener (e.g., onClick={null})
deleteListener(this, propKey);
}
} else if (this._namespaceURI === DOMNamespaces.svg) {
DOMPropertyOperations.deleteValueForSVGAttribute(
getNode(this),
propKey
);
} else if (
DOMProperty.properties[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
@@ -924,6 +931,12 @@ ReactDOMComponent.Mixin = {
propKey,
nextProp
);
} else if (this._namespaceURI === DOMNamespaces.svg) {
DOMPropertyOperations.setValueForSVGAttribute(
getNode(this),
propKey,
nextProp
);
} else if (
DOMProperty.properties[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
@@ -12,6 +12,8 @@
'use strict';
var ReactDOMUnknownPropertyDevtool = require('ReactDOMUnknownPropertyDevtool');
var ReactDOMSVGDeprecatedAttributeDevtool =
require('ReactDOMSVGDeprecatedAttributeDevtool');
var warning = require('warning');
@@ -53,14 +55,21 @@ var ReactDOMDebugTool = {
onCreateMarkupForProperty(name, value) {
emitEvent('onCreateMarkupForProperty', name, value);
},
onCreateMarkupForSVGAttribute(name, value) {
emitEvent('onCreateMarkupForSVGAttribute', name, value);
},
onSetValueForProperty(node, name, value) {
emitEvent('onSetValueForProperty', node, name, value);
},
onSetValueForSVGAttribute(node, name, value) {
emitEvent('onSetValueForSVGAttribute', node, name, value);
},
onDeleteValueForProperty(node, name) {
emitEvent('onDeleteValueForProperty', node, name);
},
};
ReactDOMDebugTool.addDevtool(ReactDOMUnknownPropertyDevtool);
ReactDOMDebugTool.addDevtool(ReactDOMSVGDeprecatedAttributeDevtool);
module.exports = ReactDOMDebugTool;
@@ -23,46 +23,19 @@ var NS = {
var SVGDOMPropertyConfig = {
Properties: {
clipPath: MUST_USE_ATTRIBUTE,
cx: MUST_USE_ATTRIBUTE,
cy: MUST_USE_ATTRIBUTE,
d: MUST_USE_ATTRIBUTE,
dx: MUST_USE_ATTRIBUTE,
dy: MUST_USE_ATTRIBUTE,
fill: MUST_USE_ATTRIBUTE,
fillOpacity: MUST_USE_ATTRIBUTE,
fontFamily: MUST_USE_ATTRIBUTE,
fontSize: MUST_USE_ATTRIBUTE,
fx: MUST_USE_ATTRIBUTE,
fy: MUST_USE_ATTRIBUTE,
gradientTransform: MUST_USE_ATTRIBUTE,
gradientUnits: MUST_USE_ATTRIBUTE,
markerEnd: MUST_USE_ATTRIBUTE,
markerMid: MUST_USE_ATTRIBUTE,
markerStart: MUST_USE_ATTRIBUTE,
offset: MUST_USE_ATTRIBUTE,
opacity: MUST_USE_ATTRIBUTE,
patternContentUnits: MUST_USE_ATTRIBUTE,
patternUnits: MUST_USE_ATTRIBUTE,
points: MUST_USE_ATTRIBUTE,
preserveAspectRatio: MUST_USE_ATTRIBUTE,
r: MUST_USE_ATTRIBUTE,
rx: MUST_USE_ATTRIBUTE,
ry: MUST_USE_ATTRIBUTE,
spreadMethod: MUST_USE_ATTRIBUTE,
stopColor: MUST_USE_ATTRIBUTE,
stopOpacity: MUST_USE_ATTRIBUTE,
stroke: MUST_USE_ATTRIBUTE,
strokeDasharray: MUST_USE_ATTRIBUTE,
strokeLinecap: MUST_USE_ATTRIBUTE,
strokeOpacity: MUST_USE_ATTRIBUTE,
strokeWidth: MUST_USE_ATTRIBUTE,
textAnchor: MUST_USE_ATTRIBUTE,
transform: MUST_USE_ATTRIBUTE,
version: MUST_USE_ATTRIBUTE,
viewBox: MUST_USE_ATTRIBUTE,
x1: MUST_USE_ATTRIBUTE,
x2: MUST_USE_ATTRIBUTE,
x: MUST_USE_ATTRIBUTE,
xlinkActuate: MUST_USE_ATTRIBUTE,
xlinkArcrole: MUST_USE_ATTRIBUTE,
xlinkHref: MUST_USE_ATTRIBUTE,
@@ -73,9 +46,6 @@ var SVGDOMPropertyConfig = {
xmlBase: MUST_USE_ATTRIBUTE,
xmlLang: MUST_USE_ATTRIBUTE,
xmlSpace: MUST_USE_ATTRIBUTE,
y1: MUST_USE_ATTRIBUTE,
y2: MUST_USE_ATTRIBUTE,
y: MUST_USE_ATTRIBUTE,
},
DOMAttributeNamespaces: {
xlinkActuate: NS.xlink,
@@ -94,15 +64,9 @@ var SVGDOMPropertyConfig = {
fillOpacity: 'fill-opacity',
fontFamily: 'font-family',
fontSize: 'font-size',
gradientTransform: 'gradientTransform',
gradientUnits: 'gradientUnits',
markerEnd: 'marker-end',
markerMid: 'marker-mid',
markerStart: 'marker-start',
patternContentUnits: 'patternContentUnits',
patternUnits: 'patternUnits',
preserveAspectRatio: 'preserveAspectRatio',
spreadMethod: 'spreadMethod',
stopColor: 'stop-color',
stopOpacity: 'stop-opacity',
strokeDasharray: 'stroke-dasharray',
@@ -110,7 +74,6 @@ var SVGDOMPropertyConfig = {
strokeOpacity: 'stroke-opacity',
strokeWidth: 'stroke-width',
textAnchor: 'text-anchor',
viewBox: 'viewBox',
xlinkActuate: 'xlink:actuate',
xlinkArcrole: 'xlink:arcrole',
xlinkHref: 'xlink:href',
@@ -260,6 +260,75 @@ describe('ReactDOMComponent', function() {
expect(container.firstChild.hasAttribute('height')).toBe(false);
});
it('should remove known SVG camel case attributes', function() {
var container = document.createElement('div');
ReactDOM.render(<svg viewBox="0 0 100 100" />, container);
expect(container.firstChild.hasAttribute('viewBox')).toBe(true);
ReactDOM.render(<svg />, container);
expect(container.firstChild.hasAttribute('viewBox')).toBe(false);
});
it('should remove known SVG hyphenated attributes', function() {
var container = document.createElement('div');
ReactDOM.render(<svg clip-path="0 0 100 100" />, container);
expect(container.firstChild.hasAttribute('clip-path')).toBe(true);
ReactDOM.render(<svg />, container);
expect(container.firstChild.hasAttribute('clip-path')).toBe(false);
});
it('should remove arbitrary SVG hyphenated attributes', function() {
var container = document.createElement('div');
ReactDOM.render(<svg the-word="the-bird" />, container);
expect(container.firstChild.hasAttribute('the-word')).toBe(true);
ReactDOM.render(<svg />, container);
expect(container.firstChild.hasAttribute('the-word')).toBe(false);
});
it('should remove arbitrary SVG camel case attributes', function() {
var container = document.createElement('div');
ReactDOM.render(<svg theWord="theBird" />, container);
expect(container.firstChild.hasAttribute('theWord')).toBe(true);
ReactDOM.render(<svg />, container);
expect(container.firstChild.hasAttribute('theWord')).toBe(false);
});
it('should remove SVG attributes that should have been hyphenated', function() {
spyOn(console, 'error');
var container = document.createElement('div');
ReactDOM.render(<svg clipPath="0 0 100 100" />, container);
expect(console.error.argsForCall.length).toBe(1);
expect(console.error.argsForCall[0][0]).toContain('clipPath');
expect(console.error.argsForCall[0][0]).toContain('clip-path');
expect(container.firstChild.hasAttribute('clip-path')).toBe(true);
ReactDOM.render(<svg />, container);
expect(container.firstChild.hasAttribute('clip-path')).toBe(false);
});
it('should remove namespaced SVG attributes', function() {
var container = document.createElement('div');
ReactDOM.render(
<svg>
<image xlinkHref="http://i.imgur.com/w7GCRPb.png" />
</svg>,
container
);
expect(container.firstChild.firstChild.hasAttributeNS(
'http://www.w3.org/1999/xlink',
'href'
)).toBe(true);
ReactDOM.render(<svg><image /></svg>, container);
expect(container.firstChild.firstChild.hasAttributeNS(
'http://www.w3.org/1999/xlink',
'href'
)).toBe(false);
});
it('should remove properties', function() {
var container = document.createElement('div');
ReactDOM.render(<div className="monkey" />, container);
@@ -331,6 +400,99 @@ describe('ReactDOMComponent', function() {
expect(container.childNodes[0].getAttribute('myattr')).toBe('myval');
});
it('should update known hyphenated attributes for SVG tags', function() {
var container = document.createElement('div');
var beforeUpdate = <svg />;
ReactDOM.render(beforeUpdate, container);
var afterUpdate = <svg clip-path="url(#starlet)" />;
ReactDOM.render(afterUpdate, container);
expect(container.childNodes[0].getAttribute('clip-path')).toBe(
'url(#starlet)'
);
});
it('should update camel case attributes for SVG tags', function() {
var container = document.createElement('div');
var beforeUpdate = <svg />;
ReactDOM.render(beforeUpdate, container);
var afterUpdate = <svg viewBox="0 0 100 100" />;
ReactDOM.render(afterUpdate, container);
expect(container.childNodes[0].getAttribute('viewBox')).toBe(
'0 0 100 100'
);
});
it('should warn camel casing hyphenated attributes for SVG tags', function() {
spyOn(console, 'error');
var container = document.createElement('div');
var beforeUpdate = <svg />;
ReactDOM.render(beforeUpdate, container);
var afterUpdate = <svg clipPath="url(#starlet)" />;
ReactDOM.render(afterUpdate, container);
expect(container.childNodes[0].getAttribute('clip-path')).toBe(
'url(#starlet)'
);
expect(console.error.argsForCall.length).toBe(1);
expect(console.error.argsForCall[0][0]).toContain('clipPath');
expect(console.error.argsForCall[0][0]).toContain('clip-path');
});
it('should update arbitrary hyphenated attributes for SVG tags', function() {
var container = document.createElement('div');
var beforeUpdate = <svg />;
ReactDOM.render(beforeUpdate, container);
var afterUpdate = <svg the-word="the-bird" />;
ReactDOM.render(afterUpdate, container);
expect(container.childNodes[0].getAttribute('the-word')).toBe('the-bird');
});
it('should update arbitrary camel case attributes for SVG tags', function() {
var container = document.createElement('div');
var beforeUpdate = <svg />;
ReactDOM.render(beforeUpdate, container);
var afterUpdate = <svg theWord="theBird" />;
ReactDOM.render(afterUpdate, container);
expect(container.childNodes[0].getAttribute('theWord')).toBe('theBird');
});
it('should update namespaced SVG attributes', function() {
var container = document.createElement('div');
var beforeUpdate = (
<svg>
<image xlinkHref="http://i.imgur.com/w7GCRPb.png" />
</svg>
);
ReactDOM.render(beforeUpdate, container);
var afterUpdate = (
<svg>
<image xlinkHref="http://i.imgur.com/JvqCM2p.png" />
</svg>
);
ReactDOM.render(afterUpdate, container);
expect(container.firstChild.firstChild.getAttributeNS(
'http://www.w3.org/1999/xlink',
'href'
)).toBe('http://i.imgur.com/JvqCM2p.png');
});
it('should clear all the styles when removing `style`', function() {
var styles = {display: 'none', color: 'red'};
var container = document.createElement('div');
@@ -0,0 +1,61 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactDOMSVGDeprecatedAttributeDevtool
*/
'use strict';
var DOMProperty = require('DOMProperty');
var warning = require('warning');
if (__DEV__) {
var reactProps = {
children: true,
dangerouslySetInnerHTML: true,
key: true,
ref: true,
};
var warnedSVGAttributes = {};
var warnDeprecatedSVGAttribute = function(name) {
if (!DOMProperty.properties.hasOwnProperty(name)) {
return;
}
if (reactProps.hasOwnProperty(name) && reactProps[name] ||
warnedSVGAttributes.hasOwnProperty(name) && warnedSVGAttributes[name]) {
return;
}
var { attributeName, attributeNamespace } = DOMProperty.properties[name];
if (attributeNamespace || name === attributeName) {
return;
}
warning(
false,
'SVG property %s is deprecated. Use the original attribute name ' +
'%s for SVG tags instead.',
name,
attributeName
);
};
}
var ReactDOMSVGDeprecatedAttributeDevtool = {
onCreateMarkupForSVGAttribute(name, value) {
warnDeprecatedSVGAttribute(name);
},
onSetValueForSVGAttribute(node, name, value) {
warnDeprecatedSVGAttribute(name);
},
};
module.exports = ReactDOMSVGDeprecatedAttributeDevtool;
@@ -82,6 +82,6 @@ var ReactDOMUnknownPropertyDevtool = {
onDeleteValueForProperty(node, name) {
warnUnknownProperty(name);
},
}
};
module.exports = ReactDOMUnknownPropertyDevtool;