diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt
index 5be728bdf9e..8d705c30e83 100644
--- a/scripts/fiber/tests-failing.txt
+++ b/scripts/fiber/tests-failing.txt
@@ -15,15 +15,6 @@ src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js
src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js
* should give context for PropType errors in nested components.
-src/renderers/art/__tests__/ReactART-test.js
-* should have the correct lifecycle state
-* should render a reasonable SVG structure in SVG mode
-* should be able to reorder components
-* should be able to reorder many components
-* renders composite with lifecycle inside group
-* resolves refs before componentDidMount
-* resolves refs before componentDidUpdate
-
src/renderers/dom/__tests__/ReactDOMProduction-test.js
* should throw with an error code in production
diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt
index 91873c59e11..f1c91f35171 100644
--- a/scripts/fiber/tests-passing.txt
+++ b/scripts/fiber/tests-passing.txt
@@ -485,6 +485,16 @@ src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js
* should warn on invalid context types
* should warn if getDefaultProps is specificed on the class
+src/renderers/art/__tests__/ReactART-test.js
+* should have the correct lifecycle state
+* should render a reasonable SVG structure in SVG mode
+* should be able to reorder components
+* should be able to reorder many components
+* renders composite with lifecycle inside group
+* resolves refs before componentDidMount
+* resolves refs before componentDidUpdate
+* adds and updates event handlers
+
src/renderers/dom/__tests__/ReactDOMProduction-test.js
* should use prod fbjs
* should use prod React
diff --git a/src/renderers/art/ReactART.js b/src/renderers/art/ReactART.js
index 0f102926e31..c8d3bebf979 100644
--- a/src/renderers/art/ReactART.js
+++ b/src/renderers/art/ReactART.js
@@ -11,631 +11,8 @@
'use strict';
-require('art/modes/current').setCurrent(
- require('art/modes/fast-noSideEffects') // Flip this to DOM mode for debugging
-);
+const ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
-const Transform = require('art/core/transform');
-const Mode = require('art/modes/current');
-
-const React = require('React');
-const ReactDOM = require('ReactDOM');
-const ReactInstanceMap = require('ReactInstanceMap');
-const ReactMultiChild = require('ReactMultiChild');
-const ReactUpdates = require('ReactUpdates');
-
-const emptyObject = require('emptyObject');
-const invariant = require('invariant');
-
-const assign = require('object-assign');
-const pooledTransform = new Transform();
-
-// Utilities
-
-function childrenAsString(children) {
- if (!children) {
- return '';
- }
- if (typeof children === 'string') {
- return children;
- }
- if (children.length) {
- return children.join('\n');
- }
- return '';
-}
-
-function createComponent(name) {
- const ReactARTComponent = function(element) {
- this.node = null;
- this.subscriptions = null;
- this.listeners = null;
- this._mountImage = null;
- this._renderedChildren = null;
- this.construct(element);
- };
- ReactARTComponent.displayName = name;
- for (let i = 1, l = arguments.length; i < l; i++) {
- assign(ReactARTComponent.prototype, arguments[i]);
- }
-
- return ReactARTComponent;
-}
-
-/**
- * Insert `node` into `parentNode` after `referenceNode`.
- */
-function injectAfter(parentNode, referenceNode, node) {
- let beforeNode;
- if (node.parentNode === parentNode &&
- node.previousSibling === referenceNode) {
- return;
- }
- if (referenceNode == null) {
- // node is supposed to be first.
- beforeNode = parentNode.firstChild;
- } else {
- // node is supposed to be after referenceNode.
- beforeNode = referenceNode.nextSibling;
- }
- if (beforeNode && beforeNode.previousSibling !== node) {
- // Cases where `node === beforeNode` should get filtered out by earlier
- // checks and the behavior isn't well-defined.
- invariant(
- node !== beforeNode,
- 'ReactART: Can not insert node before itself'
- );
- node.injectBefore(beforeNode);
- } else if (node.parentNode !== parentNode) {
- node.inject(parentNode);
- }
-}
-
-// ContainerMixin for components that can hold ART nodes
-
-const ContainerMixin = assign({}, ReactMultiChild, {
-
- /**
- * Moves a child component to the supplied index.
- *
- * @param {ReactComponent} child Component to move.
- * @param {number} toIndex Destination index of the element.
- * @protected
- */
- moveChild: function(child, afterNode, toIndex, lastIndex) {
- const childNode = child._mountImage;
- injectAfter(this.node, afterNode, childNode);
- },
-
- /**
- * Creates a child component.
- *
- * @param {ReactComponent} child Component to create.
- * @param {object} childNode ART node to insert.
- * @protected
- */
- createChild: function(child, afterNode, childNode) {
- child._mountImage = childNode;
- injectAfter(this.node, afterNode, childNode);
- },
-
- /**
- * Removes a child component.
- *
- * @param {ReactComponent} child Child to remove.
- * @protected
- */
- removeChild: function(child) {
- child._mountImage.eject();
- child._mountImage = null;
- },
-
- updateChildrenAtRoot: function(nextChildren, transaction) {
- this.updateChildren(nextChildren, transaction, emptyObject);
- },
-
- mountAndInjectChildrenAtRoot: function(children, transaction) {
- this.mountAndInjectChildren(children, transaction, emptyObject);
- },
-
- /**
- * Override to bypass batch updating because it is not necessary.
- *
- * @param {?object} nextChildren.
- * @param {ReactReconcileTransaction} transaction
- * @internal
- * @override {ReactMultiChild.updateChildren}
- */
- updateChildren: function(nextChildren, transaction, context) {
- this._updateChildren(nextChildren, transaction, context);
- },
-
- // Shorthands
-
- mountAndInjectChildren: function(children, transaction, context) {
- const mountedImages = this.mountChildren(
- children,
- transaction,
- context
- );
- // Each mount image corresponds to one of the flattened children
- let i = 0;
- for (let key in this._renderedChildren) {
- if (this._renderedChildren.hasOwnProperty(key)) {
- const child = this._renderedChildren[key];
- child._mountImage = mountedImages[i];
- mountedImages[i].inject(this.node);
- i++;
- }
- }
- }
-
-});
-
-// Surface is a React DOM Component, not an ART component. It serves as the
-// entry point into the ART reconciler.
-
-const Surface = React.createClass({
-
- displayName: 'Surface',
-
- mixins: [ContainerMixin],
-
- componentDidMount: function() {
- const domNode = ReactDOM.findDOMNode(this);
-
- this.node = Mode.Surface(+this.props.width, +this.props.height, domNode);
-
- const transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
- transaction.perform(
- this.mountAndInjectChildren,
- this,
- this.props.children,
- transaction,
- ReactInstanceMap.get(this)._context
- );
- ReactUpdates.ReactReconcileTransaction.release(transaction);
- },
-
- componentDidUpdate: function(oldProps) {
- const node = this.node;
- if (this.props.width != oldProps.width ||
- this.props.height != oldProps.height) {
- node.resize(+this.props.width, +this.props.height);
- }
-
- const transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
- transaction.perform(
- this.updateChildren,
- this,
- this.props.children,
- transaction,
- ReactInstanceMap.get(this)._context
- );
- ReactUpdates.ReactReconcileTransaction.release(transaction);
-
- if (node.render) {
- node.render();
- }
- },
-
- componentWillUnmount: function() {
- this.unmountChildren();
- },
-
- render: function() {
- // This is going to be a placeholder because we don't know what it will
- // actually resolve to because ART may render canvas, vml or svg tags here.
- // We only allow a subset of properties since others might conflict with
- // ART's properties.
- const props = this.props;
-
- // TODO: ART's Canvas Mode overrides surface title and cursor
- const Tag = Mode.Surface.tagName;
- return (
-
- );
- }
-
-});
-
-// Various nodes that can go into a surface
-
-const EventTypes = {
- onMouseMove: 'mousemove',
- onMouseOver: 'mouseover',
- onMouseOut: 'mouseout',
- onMouseUp: 'mouseup',
- onMouseDown: 'mousedown',
- onClick: 'click'
-};
-
-const NodeMixin = {
-
- construct: function(element) {
- this._currentElement = element;
- },
-
- getNativeNode: function() {
- return this.node;
- },
-
- getPublicInstance: function() {
- return this.node;
- },
-
- putEventListener: function(type, listener) {
- const subscriptions = this.subscriptions || (this.subscriptions = {});
- const listeners = this.listeners || (this.listeners = {});
- listeners[type] = listener;
- if (listener) {
- if (!subscriptions[type]) {
- subscriptions[type] = this.node.subscribe(type, listener, this);
- }
- } else {
- if (subscriptions[type]) {
- subscriptions[type]();
- delete subscriptions[type];
- }
- }
- },
-
- handleEvent: function(event) {
- const listener = this.listeners[event.type];
- if (!listener) {
- return;
- }
- if (typeof listener === 'function') {
- listener.call(this, event);
- } else if (listener.handleEvent) {
- listener.handleEvent(event);
- }
- },
-
- destroyEventListeners: function() {
- const subscriptions = this.subscriptions;
- if (subscriptions) {
- for (let type in subscriptions) {
- subscriptions[type]();
- }
- }
- this.subscriptions = null;
- this.listeners = null;
- },
-
- applyNodeProps: function(oldProps, props) {
- const node = this.node;
-
- const scaleX = props.scaleX != null ? props.scaleX :
- props.scale != null ? props.scale : 1;
- const scaleY = props.scaleY != null ? props.scaleY :
- props.scale != null ? props.scale : 1;
-
- pooledTransform
- .transformTo(1, 0, 0, 1, 0, 0)
- .move(props.x || 0, props.y || 0)
- .rotate(props.rotation || 0, props.originX, props.originY)
- .scale(scaleX, scaleY, props.originX, props.originY);
-
- if (props.transform != null) {
- pooledTransform.transform(props.transform);
- }
-
- if (node.xx !== pooledTransform.xx || node.yx !== pooledTransform.yx ||
- node.xy !== pooledTransform.xy || node.yy !== pooledTransform.yy ||
- node.x !== pooledTransform.x || node.y !== pooledTransform.y) {
- node.transformTo(pooledTransform);
- }
-
- if (props.cursor !== oldProps.cursor || props.title !== oldProps.title) {
- node.indicate(props.cursor, props.title);
- }
-
- if (node.blend && props.opacity !== oldProps.opacity) {
- node.blend(props.opacity == null ? 1 : props.opacity);
- }
-
- if (props.visible !== oldProps.visible) {
- if (props.visible == null || props.visible) {
- node.show();
- } else {
- node.hide();
- }
- }
-
- for (let type in EventTypes) {
- this.putEventListener(EventTypes[type], props[type]);
- }
- },
-
- mountComponentIntoNode: function(rootID, container) {
- throw new Error(
- 'You cannot render an ART component standalone. ' +
- 'You need to wrap it in a Surface.'
- );
- }
-
-};
-
-// Group
-
-const Group = createComponent('Group', NodeMixin, ContainerMixin, {
-
- mountComponent: function(
- transaction,
- nativeParent,
- nativeContainerInfo,
- context
- ) {
- this.node = Mode.Group();
- const props = this._currentElement.props;
- this.applyGroupProps(emptyObject, props);
- this.mountAndInjectChildren(props.children, transaction, context);
- return this.node;
- },
-
- receiveComponent: function(nextComponent, transaction, context) {
- const props = nextComponent.props;
- const oldProps = this._currentElement.props;
- this.applyGroupProps(oldProps, props);
- this.updateChildren(props.children, transaction, context);
- this._currentElement = nextComponent;
- },
-
- applyGroupProps: function(oldProps, props) {
- this.node.width = props.width;
- this.node.height = props.height;
- this.applyNodeProps(oldProps, props);
- },
-
- unmountComponent: function() {
- this.destroyEventListeners();
- this.unmountChildren();
- }
-
-});
-
-// ClippingRectangle
-const ClippingRectangle = createComponent(
- 'ClippingRectangle', NodeMixin, ContainerMixin, {
-
- mountComponent: function(
- transaction,
- nativeParent,
- nativeContainerInfo,
- context
- ) {
- this.node = Mode.ClippingRectangle();
- const props = this._currentElement.props;
- this.applyClippingProps(emptyObject, props);
- this.mountAndInjectChildren(props.children, transaction, context);
- return this.node;
- },
-
- receiveComponent: function(nextComponent, transaction, context) {
- const props = nextComponent.props;
- const oldProps = this._currentElement.props;
- this.applyClippingProps(oldProps, props);
- this.updateChildren(props.children, transaction, context);
- this._currentElement = nextComponent;
- },
-
- applyClippingProps: function(oldProps, props) {
- this.node.width = props.width;
- this.node.height = props.height;
- this.node.x = props.x;
- this.node.y = props.y;
- this.applyNodeProps(oldProps, props);
- },
-
- unmountComponent: function() {
- this.destroyEventListeners();
- this.unmountChildren();
- }
-
-});
-
-
-// Renderables
-
-const RenderableMixin = assign({}, NodeMixin, {
-
- applyRenderableProps: function(oldProps, props) {
- if (oldProps.fill !== props.fill) {
- if (props.fill && props.fill.applyFill) {
- props.fill.applyFill(this.node);
- } else {
- this.node.fill(props.fill);
- }
- }
- if (
- oldProps.stroke !== props.stroke ||
- oldProps.strokeWidth !== props.strokeWidth ||
- oldProps.strokeCap !== props.strokeCap ||
- oldProps.strokeJoin !== props.strokeJoin ||
- // TODO: Consider a deep check of stokeDash.
- // This may benefit the VML version in IE.
- oldProps.strokeDash !== props.strokeDash
- ) {
- this.node.stroke(
- props.stroke,
- props.strokeWidth,
- props.strokeCap,
- props.strokeJoin,
- props.strokeDash
- );
- }
- this.applyNodeProps(oldProps, props);
- },
-
- unmountComponent: function() {
- this.destroyEventListeners();
- }
-
-});
-
-// Shape
-
-const Shape = createComponent('Shape', RenderableMixin, {
-
- construct: function(element) {
- this._currentElement = element;
- this._oldDelta = null;
- this._oldPath = null;
- },
-
- mountComponent: function(
- transaction,
- nativeParent,
- nativeContainerInfo,
- context
- ) {
- this.node = Mode.Shape();
- const props = this._currentElement.props;
- this.applyShapeProps(emptyObject, props);
- return this.node;
- },
-
- receiveComponent: function(nextComponent, transaction, context) {
- const props = nextComponent.props;
- const oldProps = this._currentElement.props;
- this.applyShapeProps(oldProps, props);
- this._currentElement = nextComponent;
- },
-
- applyShapeProps: function(oldProps, props) {
- const oldDelta = this._oldDelta;
- const oldPath = this._oldPath;
- const path = props.d || childrenAsString(props.children);
-
- if (path.delta !== oldDelta ||
- path !== oldPath ||
- oldProps.width !== props.width ||
- oldProps.height !== props.height) {
-
- this.node.draw(
- path,
- props.width,
- props.height
- );
-
- this._oldPath = path;
- this._oldDelta = path.delta;
- }
-
- this.applyRenderableProps(oldProps, props);
- }
-
-});
-
-// Text
-
-const Text = createComponent('Text', RenderableMixin, {
-
- construct: function(element) {
- this._currentElement = element;
- this._oldString = null;
- },
-
- mountComponent: function(
- transaction,
- nativeParent,
- nativeContainerInfo,
- context
- ) {
- const props = this._currentElement.props;
- const newString = childrenAsString(props.children);
- this.node = Mode.Text(newString, props.font, props.alignment, props.path);
- this._oldString = newString;
- this.applyRenderableProps(emptyObject, props);
- return this.node;
- },
-
- isSameFont: function(oldFont, newFont) {
- if (oldFont === newFont) {
- return true;
- }
- if (typeof newFont === 'string' || typeof oldFont === 'string') {
- return false;
- }
- return (
- newFont.fontSize === oldFont.fontSize &&
- newFont.fontStyle === oldFont.fontStyle &&
- newFont.fontVariant === oldFont.fontVariant &&
- newFont.fontWeight === oldFont.fontWeight &&
- newFont.fontFamily === oldFont.fontFamily
- );
- },
-
- receiveComponent: function(nextComponent, transaction, context) {
- const props = nextComponent.props;
- const oldProps = this._currentElement.props;
-
- const oldString = this._oldString;
- const newString = childrenAsString(props.children);
-
- if (oldString !== newString ||
- !this.isSameFont(oldProps.font, props.font) ||
- oldProps.alignment !== props.alignment ||
- oldProps.path !== props.path) {
- this.node.draw(
- newString,
- props.font,
- props.alignment,
- props.path
- );
- this._oldString = newString;
- }
-
- this.applyRenderableProps(oldProps, props);
- this._currentElement = nextComponent;
- }
-
-});
-
-// Declarative fill type objects - API design not finalized
-
-const slice = Array.prototype.slice;
-
-function LinearGradient(stops, x1, y1, x2, y2) {
- this.args = slice.call(arguments);
-}
-
-LinearGradient.prototype.applyFill = function(node) {
- node.fillLinear.apply(node, this.args);
-};
-
-function RadialGradient(stops, fx, fy, rx, ry, cx, cy) {
- this.args = slice.call(arguments);
-}
-
-RadialGradient.prototype.applyFill = function(node) {
- node.fillRadial.apply(node, this.args);
-};
-
-function Pattern(url, width, height, left, top) {
- this.args = slice.call(arguments);
-}
-
-Pattern.prototype.applyFill = function(node) {
- node.fillImage.apply(node, this.args);
-};
-
-module.exports = {
- ClippingRectangle,
- Group,
- LinearGradient,
- Path: Mode.Path,
- Pattern,
- RadialGradient,
- Shape,
- Surface,
- Text,
- Transform,
-};
+module.exports = ReactDOMFeatureFlags.useFiber
+ ? require('ReactARTFiber')
+ : require('ReactARTStack');
diff --git a/src/renderers/art/ReactARTFiber.js b/src/renderers/art/ReactARTFiber.js
new file mode 100644
index 00000000000..467f9764634
--- /dev/null
+++ b/src/renderers/art/ReactARTFiber.js
@@ -0,0 +1,515 @@
+/**
+ * Copyright (c) 2013-present 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 ReactARTFiber
+ */
+'use strict';
+
+require('art/modes/current').setCurrent(
+ // Change to 'art/modes/dom' for easier debugging via SVG
+ require('art/modes/fast-noSideEffects'),
+);
+
+const Mode = require('art/modes/current');
+const Transform = require('art/core/transform');
+const invariant = require('fbjs/lib/invariant');
+const React = require('React');
+const ReactFiberReconciler = require('ReactFiberReconciler');
+
+const { Component } = React;
+
+const pooledTransform = new Transform();
+
+const EVENT_TYPES = {
+ onClick: 'click',
+ onMouseMove: 'mousemove',
+ onMouseOver: 'mouseover',
+ onMouseOut: 'mouseout',
+ onMouseUp: 'mouseup',
+ onMouseDown: 'mousedown',
+};
+
+const TYPES = {
+ CLIPPING_RECTANGLE: 'ClippingRectangle',
+ GROUP: 'Group',
+ SHAPE: 'Shape',
+ TEXT: 'Text',
+};
+
+/** Helper Methods */
+
+function addEventListeners(instance, type, listener) {
+ // We need to explicitly unregister before unmount.
+ // For this reason we need to track subscriptions.
+ if (!instance._listeners) {
+ instance._listeners = {};
+ instance._subscriptions = {};
+ }
+
+ instance._listeners[type] = listener;
+
+ if (listener) {
+ if (!instance._subscriptions[type]) {
+ instance._subscriptions[type] = instance.subscribe(
+ type,
+ createEventHandler(instance),
+ instance,
+ );
+ }
+ } else {
+ if (instance._subscriptions[type]) {
+ instance._subscriptions[type]();
+ delete instance._subscriptions[type];
+ }
+ }
+}
+
+function childrenAsString(children) {
+ if (!children) {
+ return '';
+ } else if (typeof children === 'string') {
+ return children;
+ } else if (children.length) {
+ return children.join('');
+ } else {
+ return '';
+ }
+}
+
+function createEventHandler(instance) {
+ return function handleEvent(event) {
+ const listener = instance._listeners[event.type];
+
+ if (!listener) {
+ // Noop
+ } else if (typeof listener === 'function') {
+ listener.call(instance, event);
+ } else if (listener.handleEvent) {
+ listener.handleEvent(event);
+ }
+ };
+}
+
+function destroyEventListeners(instance) {
+ if (instance._subscriptions) {
+ for (let type in instance._subscriptions) {
+ instance._subscriptions[type]();
+ }
+ }
+
+ instance._subscriptions = null;
+ instance._listeners = null;
+}
+
+function getScaleX(props) {
+ if (props.scaleX != null) {
+ return props.scaleX;
+ } else if (props.scale != null) {
+ return props.scale;
+ } else {
+ return 1;
+ }
+}
+
+function getScaleY(props) {
+ if (props.scaleY != null) {
+ return props.scaleY;
+ } else if (props.scale != null) {
+ return props.scale;
+ } else {
+ return 1;
+ }
+}
+
+function isSameFont(oldFont, newFont) {
+ if (oldFont === newFont) {
+ return true;
+ } else if (
+ typeof newFont === 'string' ||
+ typeof oldFont === 'string'
+ ) {
+ return false;
+ } else {
+ return (
+ newFont.fontSize === oldFont.fontSize &&
+ newFont.fontStyle === oldFont.fontStyle &&
+ newFont.fontVariant === oldFont.fontVariant &&
+ newFont.fontWeight === oldFont.fontWeight &&
+ newFont.fontFamily === oldFont.fontFamily
+ );
+ }
+}
+
+/** Render Methods */
+
+function applyClippingRectangleProps(instance, props, prevProps = {}) {
+ applyNodeProps(instance, props, prevProps);
+
+ instance.width = props.width;
+ instance.height = props.height;
+}
+
+function applyGroupProps(instance, props, prevProps = {}) {
+ applyNodeProps(instance, props, prevProps);
+
+ instance.width = props.width;
+ instance.height = props.height;
+}
+
+function applyNodeProps(instance, props, prevProps = {}) {
+ const scaleX = getScaleX(props);
+ const scaleY = getScaleY(props);
+
+ pooledTransform
+ .transformTo(1, 0, 0, 1, 0, 0)
+ .move(props.x || 0, props.y || 0)
+ .rotate(props.rotation || 0, props.originX, props.originY)
+ .scale(scaleX, scaleY, props.originX, props.originY);
+
+ if (props.transform != null) {
+ pooledTransform.transform(props.transform);
+ }
+
+ if (
+ instance.xx !== pooledTransform.xx || instance.yx !== pooledTransform.yx ||
+ instance.xy !== pooledTransform.xy || instance.yy !== pooledTransform.yy ||
+ instance.x !== pooledTransform.x || instance.y !== pooledTransform.y
+ ) {
+ instance.transformTo(pooledTransform);
+ }
+
+ if (
+ props.cursor !== prevProps.cursor ||
+ props.title !== prevProps.title
+ ) {
+ instance.indicate(props.cursor, props.title);
+ }
+
+ if (
+ instance.blend &&
+ props.opacity !== prevProps.opacity
+ ) {
+ instance.blend(props.opacity == null ? 1 : props.opacity);
+ }
+
+ if (props.visible !== prevProps.visible) {
+ if (props.visible == null || props.visible) {
+ instance.show();
+ } else {
+ instance.hide();
+ }
+ }
+
+ for (let type in EVENT_TYPES) {
+ addEventListeners(instance, EVENT_TYPES[type], props[type]);
+ }
+}
+
+function applyRenderableNodeProps(instance, props, prevProps = {}) {
+ applyNodeProps(instance, props, prevProps);
+
+ if (prevProps.fill !== props.fill) {
+ if (props.fill && props.fill.applyFill) {
+ props.fill.applyFill(instance);
+ } else {
+ instance.fill(props.fill);
+ }
+ }
+ if (
+ prevProps.stroke !== props.stroke ||
+ prevProps.strokeWidth !== props.strokeWidth ||
+ prevProps.strokeCap !== props.strokeCap ||
+ prevProps.strokeJoin !== props.strokeJoin ||
+ // TODO: Consider deep check of stokeDash; may benefit VML in IE.
+ prevProps.strokeDash !== props.strokeDash
+ ) {
+ instance.stroke(
+ props.stroke,
+ props.strokeWidth,
+ props.strokeCap,
+ props.strokeJoin,
+ props.strokeDash,
+ );
+ }
+}
+
+function applyShapeProps(instance, props, prevProps = {}) {
+ applyRenderableNodeProps(instance, props, prevProps);
+
+ const path = props.d || childrenAsString(props.children);
+
+ const prevDelta = instance._prevDelta;
+ const prevPath = instance._prevPath;
+
+ if (
+ path !== prevPath ||
+ path.delta !== prevDelta ||
+ prevProps.height !== props.height ||
+ prevProps.width !== props.width
+ ) {
+ instance.draw(
+ path,
+ props.width,
+ props.height,
+ );
+
+ instance._prevDelta = path.delta;
+ instance._prevPath = path;
+ }
+}
+
+function applyTextProps(instance, props, prevProps = {}) {
+ applyRenderableNodeProps(instance, props, prevProps);
+
+ const string = childrenAsString(props.children);
+
+ if (
+ instance._currentString !== string ||
+ !isSameFont(props.font, prevProps.font) ||
+ props.alignment !== prevProps.alignment ||
+ props.path !== prevProps.path
+ ) {
+ instance.draw(
+ string,
+ props.font,
+ props.alignment,
+ props.path,
+ );
+
+ instance._currentString = string;
+ }
+}
+
+/** Declarative fill-type objects; API design not finalized */
+
+const slice = Array.prototype.slice;
+
+class LinearGradient {
+ constructor(stops, x1, y1, x2, y2) {
+ this._args = slice.call(arguments);
+ }
+
+ applyFill(node) {
+ node.fillLinear.apply(node, this._args);
+ }
+}
+
+class RadialGradient {
+ constructor(stops, fx, fy, rx, ry, cx, cy) {
+ this._args = slice.call(arguments);
+ }
+
+ applyFill(node) {
+ node.fillRadial.apply(node, this.args);
+ }
+}
+
+class Pattern {
+ constructor(url, width, height, left, top) {
+ this._args = slice.call(arguments);
+ }
+
+ applyFill(node) {
+ node.fillImage.apply(node, this.args);
+ }
+}
+
+/** React Components */
+
+class Surface extends Component {
+ componentDidMount() {
+ const { height, width } = this.props;
+
+ this._surface = Mode.Surface(+width, +height, this._tagRef);
+
+ this._mountNode = ARTRenderer.mountContainer(
+ this.props.children,
+ this._surface,
+ this,
+ );
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ const props = this.props;
+
+ if (
+ props.height !== prevProps.height ||
+ props.width !== prevProps.width
+ ) {
+ this._surface.resize(+props.width, +props.height);
+ }
+
+ ARTRenderer.updateContainer(
+ this.props.children,
+ this._mountNode,
+ this,
+ );
+
+ if (this._surface.render) {
+ this._surface.render();
+ }
+ }
+
+ componentWillUnmount() {
+ ARTRenderer.unmountContainer(this._mountNode);
+ }
+
+ render() {
+ // This is going to be a placeholder because we don't know what it will
+ // actually resolve to because ART may render canvas, vml or svg tags here.
+ // We only allow a subset of properties since others might conflict with
+ // ART's properties.
+ const props = this.props;
+
+ // TODO: ART's Canvas Mode overrides surface title and cursor
+ const Tag = Mode.Surface.tagName;
+
+ return (
+ this._tagRef = ref}
+ accessKey={props.accessKey}
+ className={props.className}
+ draggable={props.draggable}
+ role={props.role}
+ style={props.style}
+ tabIndex={props.tabIndex}
+ title={props.title}
+ />
+ );
+ }
+}
+
+/** ART Renderer */
+
+const ARTRenderer = ReactFiberReconciler({
+ appendChild(parentInstance, child) {
+ if (child.parentNode === parentInstance) {
+ child.eject();
+ }
+
+ child.inject(parentInstance);
+ },
+
+ appendInitialChild(parentInstance, child) {
+ if (typeof child === 'string') {
+ // Noop for string children of Text (eg {'foo'}{'bar'})
+ return;
+ }
+
+ child.inject(parentInstance);
+ },
+
+ commitTextUpdate(textInstance, oldText, newText) {
+ // Noop
+ },
+
+ commitUpdate(instance, oldProps, newProps) {
+ instance._applyProps(instance, newProps, oldProps);
+ },
+
+ createInstance(type, props, internalInstanceHandle) {
+ let instance;
+
+ switch (type) {
+ case TYPES.CLIPPING_RECTANGLE:
+ instance = Mode.ClippingRectangle();
+ instance._applyProps = applyClippingRectangleProps;
+ break;
+ case TYPES.GROUP:
+ instance = Mode.Group();
+ instance._applyProps = applyGroupProps;
+ break;
+ case TYPES.SHAPE:
+ instance = Mode.Shape();
+ instance._applyProps = applyShapeProps;
+ break;
+ case TYPES.TEXT:
+ instance = Mode.Text(
+ childrenAsString(props.children),
+ props.font,
+ props.alignment,
+ props.path,
+ );
+ instance._applyProps = applyTextProps;
+ break;
+ }
+
+ invariant(instance, 'ReactART does not support the type "%s"', type);
+
+ instance._applyProps(instance, props);
+
+ return instance;
+ },
+
+ createTextInstance(text, internalInstanceHandle) {
+ return text;
+ },
+
+ finalizeInitialChildren(domElement, type, props) {
+ // Noop
+ },
+
+ insertBefore(parentInstance, child, beforeChild) {
+ invariant(
+ child !== beforeChild,
+ 'ReactART: Can not insert node before itself'
+ );
+
+ child.injectBefore(beforeChild);
+ },
+
+ prepareForCommit() {
+ // Noop
+ },
+
+ prepareUpdate(domElement, oldProps, newProps) {
+ return true;
+ },
+
+ removeChild(parentInstance, child) {
+ destroyEventListeners(child);
+
+ child.eject();
+ },
+
+ resetAfterCommit() {
+ // Noop
+ },
+
+ resetTextContent(domElement) {
+ // Noop
+ },
+
+ scheduleAnimationCallback: window.requestAnimationFrame,
+
+ scheduleDeferredCallback: window.requestIdleCallback,
+
+ shouldSetTextContent(props) {
+ return (
+ typeof props.children === 'string' ||
+ typeof props.children === 'number'
+ );
+ },
+
+ useSyncScheduling: true,
+});
+
+/** API */
+
+module.exports = {
+ ClippingRectangle: TYPES.CLIPPING_RECTANGLE,
+ Group: TYPES.GROUP,
+ LinearGradient,
+ Path: Mode.Path,
+ Pattern,
+ RadialGradient,
+ Shape: TYPES.SHAPE,
+ Surface,
+ Text: TYPES.TEXT,
+ Transform,
+};
diff --git a/src/renderers/art/ReactARTStack.js b/src/renderers/art/ReactARTStack.js
new file mode 100644
index 00000000000..0c17c714739
--- /dev/null
+++ b/src/renderers/art/ReactARTStack.js
@@ -0,0 +1,643 @@
+/**
+ * Copyright (c) 2013-present 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 ReactARTStack
+ */
+
+'use strict';
+
+require('art/modes/current').setCurrent(
+ require('art/modes/fast-noSideEffects') // Flip this to DOM mode for debugging
+);
+
+const Transform = require('art/core/transform');
+const Mode = require('art/modes/current');
+
+const React = require('React');
+const ReactDOM = require('ReactDOM');
+const ReactInstanceMap = require('ReactInstanceMap');
+const ReactMultiChild = require('ReactMultiChild');
+const ReactUpdates = require('ReactUpdates');
+
+const emptyObject = require('emptyObject');
+const invariant = require('invariant');
+
+const assign = require('object-assign');
+const pooledTransform = new Transform();
+
+// Utilities
+
+function childrenAsString(children) {
+ if (!children) {
+ return '';
+ }
+ if (typeof children === 'string') {
+ return children;
+ }
+ if (children.length) {
+ return children.join('\n');
+ }
+ return '';
+}
+
+function createComponent(name) {
+ const ReactARTComponent = function(element) {
+ this.node = null;
+ this.subscriptions = null;
+ this.listeners = null;
+ this._mountImage = null;
+ this._renderedChildren = null;
+ this.construct(element);
+ };
+ ReactARTComponent.displayName = name;
+ for (let i = 1, l = arguments.length; i < l; i++) {
+ assign(ReactARTComponent.prototype, arguments[i]);
+ }
+
+ return ReactARTComponent;
+}
+
+/**
+ * Insert `node` into `parentNode` after `referenceNode`.
+ */
+function injectAfter(parentNode, referenceNode, node) {
+ let beforeNode;
+ if (node.parentNode === parentNode &&
+ node.previousSibling === referenceNode) {
+ return;
+ }
+ if (referenceNode == null) {
+ // node is supposed to be first.
+ beforeNode = parentNode.firstChild;
+ } else {
+ // node is supposed to be after referenceNode.
+ beforeNode = referenceNode.nextSibling;
+ }
+ if (beforeNode && beforeNode.previousSibling !== node) {
+ // Cases where `node === beforeNode` should get filtered out by earlier
+ // checks and the behavior isn't well-defined.
+ invariant(
+ node !== beforeNode,
+ 'ReactART: Can not insert node before itself'
+ );
+ node.injectBefore(beforeNode);
+ } else if (node.parentNode !== parentNode) {
+ node.inject(parentNode);
+ }
+}
+
+// ContainerMixin for components that can hold ART nodes
+
+const ContainerMixin = assign({}, ReactMultiChild, {
+
+ /**
+ * Moves a child component to the supplied index.
+ *
+ * @param {ReactComponent} child Component to move.
+ * @param {number} toIndex Destination index of the element.
+ * @protected
+ */
+ moveChild: function(child, afterNode, toIndex, lastIndex) {
+ const childNode = child._mountImage;
+ injectAfter(this.node, afterNode, childNode);
+ },
+
+ /**
+ * Creates a child component.
+ *
+ * @param {ReactComponent} child Component to create.
+ * @param {object} childNode ART node to insert.
+ * @protected
+ */
+ createChild: function(child, afterNode, childNode) {
+ child._mountImage = childNode;
+ injectAfter(this.node, afterNode, childNode);
+ },
+
+ /**
+ * Removes a child component.
+ *
+ * @param {ReactComponent} child Child to remove.
+ * @protected
+ */
+ removeChild: function(child) {
+ child._mountImage.eject();
+ child._mountImage = null;
+ },
+
+ updateChildrenAtRoot: function(nextChildren, transaction) {
+ this.updateChildren(nextChildren, transaction, emptyObject);
+ },
+
+ mountAndInjectChildrenAtRoot: function(children, transaction) {
+ this.mountAndInjectChildren(children, transaction, emptyObject);
+ },
+
+ /**
+ * Override to bypass batch updating because it is not necessary.
+ *
+ * @param {?object} nextChildren.
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ * @override {ReactMultiChild.updateChildren}
+ */
+ updateChildren: function(nextChildren, transaction, context) {
+ this._updateChildren(nextChildren, transaction, context);
+ },
+
+ // Shorthands
+
+ mountAndInjectChildren: function(children, transaction, context) {
+ const mountedImages = this.mountChildren(
+ children,
+ transaction,
+ context
+ );
+ // Each mount image corresponds to one of the flattened children
+ let i = 0;
+ for (let key in this._renderedChildren) {
+ if (this._renderedChildren.hasOwnProperty(key)) {
+ const child = this._renderedChildren[key];
+ child._mountImage = mountedImages[i];
+ mountedImages[i].inject(this.node);
+ i++;
+ }
+ }
+ }
+
+});
+
+// Surface is a React DOM Component, not an ART component. It serves as the
+// entry point into the ART reconciler.
+
+const Surface = React.createClass({
+
+ displayName: 'Surface',
+
+ mixins: [ContainerMixin],
+
+ componentDidMount: function() {
+ const domNode = ReactDOM.findDOMNode(this);
+
+ this.node = Mode.Surface(+this.props.width, +this.props.height, domNode);
+
+ const transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
+ transaction.perform(
+ this.mountAndInjectChildren,
+ this,
+ this.props.children,
+ transaction,
+ ReactInstanceMap.get(this)._context
+ );
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
+ },
+
+ componentDidUpdate: function(oldProps) {
+ const node = this.node;
+ if (this.props.width != oldProps.width ||
+ this.props.height != oldProps.height) {
+ node.resize(+this.props.width, +this.props.height);
+ }
+
+ const transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
+ transaction.perform(
+ this.updateChildren,
+ this,
+ this.props.children,
+ transaction,
+ ReactInstanceMap.get(this)._context
+ );
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
+
+ if (node.render) {
+ node.render();
+ }
+ },
+
+ componentWillUnmount: function() {
+ this.unmountChildren();
+ },
+
+ render: function() {
+ // This is going to be a placeholder because we don't know what it will
+ // actually resolve to because ART may render canvas, vml or svg tags here.
+ // We only allow a subset of properties since others might conflict with
+ // ART's properties.
+ const props = this.props;
+
+ // TODO: ART's Canvas Mode overrides surface title and cursor
+ const Tag = Mode.Surface.tagName;
+ return (
+
+ );
+ }
+
+});
+
+// Various nodes that can go into a surface
+
+const EventTypes = {
+ onMouseMove: 'mousemove',
+ onMouseOver: 'mouseover',
+ onMouseOut: 'mouseout',
+ onMouseUp: 'mouseup',
+ onMouseDown: 'mousedown',
+ onClick: 'click'
+};
+
+const NodeMixin = {
+
+ construct: function(element) {
+ this._currentElement = element;
+ },
+
+ getNativeNode: function() {
+ return this.node;
+ },
+
+ getPublicInstance: function() {
+ return this.node;
+ },
+
+ putEventListener: function(type, listener) {
+ const subscriptions = this.subscriptions || (this.subscriptions = {});
+ const listeners = this.listeners || (this.listeners = {});
+ listeners[type] = listener;
+ if (listener) {
+ if (!subscriptions[type]) {
+ subscriptions[type] = this.node.subscribe(type, this.handleEvent, this);
+ }
+ } else {
+ if (subscriptions[type]) {
+ subscriptions[type]();
+ delete subscriptions[type];
+ }
+ }
+ },
+
+ handleEvent: function(event) {
+ const listener = this.listeners[event.type];
+ if (!listener) {
+ return;
+ }
+ if (typeof listener === 'function') {
+ listener.call(this, event);
+ } else if (listener.handleEvent) {
+ listener.handleEvent(event);
+ }
+ },
+
+ destroyEventListeners: function() {
+ const subscriptions = this.subscriptions;
+ if (subscriptions) {
+ for (let type in subscriptions) {
+ subscriptions[type]();
+ }
+ }
+ this.subscriptions = null;
+ this.listeners = null;
+ },
+
+ applyNodeProps: function(oldProps, props) {
+ const node = this.node;
+
+ const scaleX = props.scaleX != null ? props.scaleX :
+ props.scale != null ? props.scale : 1;
+ const scaleY = props.scaleY != null ? props.scaleY :
+ props.scale != null ? props.scale : 1;
+
+ pooledTransform
+ .transformTo(1, 0, 0, 1, 0, 0)
+ .move(props.x || 0, props.y || 0)
+ .rotate(props.rotation || 0, props.originX, props.originY)
+ .scale(scaleX, scaleY, props.originX, props.originY);
+
+ if (props.transform != null) {
+ pooledTransform.transform(props.transform);
+ }
+
+ if (node.xx !== pooledTransform.xx || node.yx !== pooledTransform.yx ||
+ node.xy !== pooledTransform.xy || node.yy !== pooledTransform.yy ||
+ node.x !== pooledTransform.x || node.y !== pooledTransform.y) {
+ node.transformTo(pooledTransform);
+ }
+
+ if (props.cursor !== oldProps.cursor || props.title !== oldProps.title) {
+ node.indicate(props.cursor, props.title);
+ }
+
+ if (node.blend && props.opacity !== oldProps.opacity) {
+ node.blend(props.opacity == null ? 1 : props.opacity);
+ }
+
+ if (props.visible !== oldProps.visible) {
+ if (props.visible == null || props.visible) {
+ node.show();
+ } else {
+ node.hide();
+ }
+ }
+
+ for (let type in EventTypes) {
+ this.putEventListener(EventTypes[type], props[type]);
+ }
+ },
+
+ mountComponentIntoNode: function(rootID, container) {
+ throw new Error(
+ 'You cannot render an ART component standalone. ' +
+ 'You need to wrap it in a Surface.'
+ );
+ }
+
+};
+
+// Group
+
+const Group = createComponent('Group', NodeMixin, ContainerMixin, {
+
+ mountComponent: function(
+ transaction,
+ nativeParent,
+ nativeContainerInfo,
+ context
+ ) {
+ this.node = Mode.Group();
+ const props = this._currentElement.props;
+ this.applyGroupProps(emptyObject, props);
+ this.mountAndInjectChildren(props.children, transaction, context);
+ return this.node;
+ },
+
+ receiveComponent: function(nextComponent, transaction, context) {
+ const props = nextComponent.props;
+ const oldProps = this._currentElement.props;
+ this.applyGroupProps(oldProps, props);
+ this.updateChildren(props.children, transaction, context);
+ this._currentElement = nextComponent;
+ },
+
+ applyGroupProps: function(oldProps, props) {
+ this.node.width = props.width;
+ this.node.height = props.height;
+ this.applyNodeProps(oldProps, props);
+ },
+
+ unmountComponent: function() {
+ this.destroyEventListeners();
+ this.unmountChildren();
+ }
+
+});
+
+// ClippingRectangle
+const ClippingRectangle = createComponent(
+ 'ClippingRectangle', NodeMixin, ContainerMixin, {
+
+ mountComponent: function(
+ transaction,
+ nativeParent,
+ nativeContainerInfo,
+ context
+ ) {
+ this.node = Mode.ClippingRectangle();
+ const props = this._currentElement.props;
+ this.applyClippingProps(emptyObject, props);
+ this.mountAndInjectChildren(props.children, transaction, context);
+ return this.node;
+ },
+
+ receiveComponent: function(nextComponent, transaction, context) {
+ const props = nextComponent.props;
+ const oldProps = this._currentElement.props;
+ this.applyClippingProps(oldProps, props);
+ this.updateChildren(props.children, transaction, context);
+ this._currentElement = nextComponent;
+ },
+
+ applyClippingProps: function(oldProps, props) {
+ this.node.width = props.width;
+ this.node.height = props.height;
+ this.node.x = props.x;
+ this.node.y = props.y;
+ this.applyNodeProps(oldProps, props);
+ },
+
+ unmountComponent: function() {
+ this.destroyEventListeners();
+ this.unmountChildren();
+ }
+
+});
+
+
+// Renderables
+
+const RenderableMixin = assign({}, NodeMixin, {
+
+ applyRenderableProps: function(oldProps, props) {
+ if (oldProps.fill !== props.fill) {
+ if (props.fill && props.fill.applyFill) {
+ props.fill.applyFill(this.node);
+ } else {
+ this.node.fill(props.fill);
+ }
+ }
+ if (
+ oldProps.stroke !== props.stroke ||
+ oldProps.strokeWidth !== props.strokeWidth ||
+ oldProps.strokeCap !== props.strokeCap ||
+ oldProps.strokeJoin !== props.strokeJoin ||
+ // TODO: Consider a deep check of stokeDash.
+ // This may benefit the VML version in IE.
+ oldProps.strokeDash !== props.strokeDash
+ ) {
+ this.node.stroke(
+ props.stroke,
+ props.strokeWidth,
+ props.strokeCap,
+ props.strokeJoin,
+ props.strokeDash
+ );
+ }
+ this.applyNodeProps(oldProps, props);
+ },
+
+ unmountComponent: function() {
+ this.destroyEventListeners();
+ }
+
+});
+
+// Shape
+
+const Shape = createComponent('Shape', RenderableMixin, {
+
+ construct: function(element) {
+ this._currentElement = element;
+ this._oldDelta = null;
+ this._oldPath = null;
+ },
+
+ mountComponent: function(
+ transaction,
+ nativeParent,
+ nativeContainerInfo,
+ context
+ ) {
+ this.node = Mode.Shape();
+ const props = this._currentElement.props;
+ this.applyShapeProps(emptyObject, props);
+ return this.node;
+ },
+
+ receiveComponent: function(nextComponent, transaction, context) {
+ const props = nextComponent.props;
+ const oldProps = this._currentElement.props;
+ this.applyShapeProps(oldProps, props);
+ this._currentElement = nextComponent;
+ },
+
+ applyShapeProps: function(oldProps, props) {
+ const oldDelta = this._oldDelta;
+ const oldPath = this._oldPath;
+ const path = props.d || childrenAsString(props.children);
+
+ if (path.delta !== oldDelta ||
+ path !== oldPath ||
+ oldProps.width !== props.width ||
+ oldProps.height !== props.height) {
+
+ this.node.draw(
+ path,
+ props.width,
+ props.height
+ );
+
+ this._oldPath = path;
+ this._oldDelta = path.delta;
+ }
+
+ this.applyRenderableProps(oldProps, props);
+ }
+
+});
+
+// Text
+
+const Text = createComponent('Text', RenderableMixin, {
+
+ construct: function(element) {
+ this._currentElement = element;
+ this._oldString = null;
+ },
+
+ mountComponent: function(
+ transaction,
+ nativeParent,
+ nativeContainerInfo,
+ context
+ ) {
+ const props = this._currentElement.props;
+ const newString = childrenAsString(props.children);
+ this.node = Mode.Text(newString, props.font, props.alignment, props.path);
+ this._oldString = newString;
+ this.applyRenderableProps(emptyObject, props);
+ return this.node;
+ },
+
+ isSameFont: function(oldFont, newFont) {
+ if (oldFont === newFont) {
+ return true;
+ }
+ if (typeof newFont === 'string' || typeof oldFont === 'string') {
+ return false;
+ }
+ return (
+ newFont.fontSize === oldFont.fontSize &&
+ newFont.fontStyle === oldFont.fontStyle &&
+ newFont.fontVariant === oldFont.fontVariant &&
+ newFont.fontWeight === oldFont.fontWeight &&
+ newFont.fontFamily === oldFont.fontFamily
+ );
+ },
+
+ receiveComponent: function(nextComponent, transaction, context) {
+ const props = nextComponent.props;
+ const oldProps = this._currentElement.props;
+
+ const oldString = this._oldString;
+ const newString = childrenAsString(props.children);
+
+ if (oldString !== newString ||
+ !this.isSameFont(oldProps.font, props.font) ||
+ oldProps.alignment !== props.alignment ||
+ oldProps.path !== props.path) {
+ this.node.draw(
+ newString,
+ props.font,
+ props.alignment,
+ props.path
+ );
+ this._oldString = newString;
+ }
+
+ this.applyRenderableProps(oldProps, props);
+ this._currentElement = nextComponent;
+ }
+
+});
+
+// Declarative fill type objects - API design not finalized
+
+const slice = Array.prototype.slice;
+
+function LinearGradient(stops, x1, y1, x2, y2) {
+ this.args = slice.call(arguments);
+}
+
+LinearGradient.prototype.applyFill = function(node) {
+ node.fillLinear.apply(node, this.args);
+};
+
+function RadialGradient(stops, fx, fy, rx, ry, cx, cy) {
+ this.args = slice.call(arguments);
+}
+
+RadialGradient.prototype.applyFill = function(node) {
+ node.fillRadial.apply(node, this.args);
+};
+
+function Pattern(url, width, height, left, top) {
+ this.args = slice.call(arguments);
+}
+
+Pattern.prototype.applyFill = function(node) {
+ node.fillImage.apply(node, this.args);
+};
+
+module.exports = {
+ ClippingRectangle,
+ Group,
+ LinearGradient,
+ Path: Mode.Path,
+ Pattern,
+ RadialGradient,
+ Shape,
+ Surface,
+ Text,
+ Transform,
+};
diff --git a/src/renderers/art/__tests__/ReactART-test.js b/src/renderers/art/__tests__/ReactART-test.js
index b8cfea1c24b..f628627adbb 100644
--- a/src/renderers/art/__tests__/ReactART-test.js
+++ b/src/renderers/art/__tests__/ReactART-test.js
@@ -295,4 +295,33 @@ describe('ReactART', () => {
expect(ref.constructor).toBe(CustomShape);
});
+ it('adds and updates event handlers', () => {
+ const container = document.createElement('div');
+
+ function render(onClick) {
+ return ReactDOM.render(
+
+
+ ,
+ container,
+ );
+ }
+
+ function doClick(instance) {
+ const path = ReactDOM.findDOMNode(instance).querySelector('path');
+
+ // ReactTestUtils.Simulate.click doesn't work with SVG elements
+ path.click();
+ }
+
+ const onClick1 = jest.fn();
+ let instance = render(onClick1);
+ doClick(instance);
+ expect(onClick1).toBeCalled();
+
+ const onClick2 = jest.fn();
+ instance = render(onClick2);
+ doClick(instance);
+ expect(onClick2).toBeCalled();
+ });
});