From 2b03b1ac9cca8743cd444602c3d2f5c14acd1303 Mon Sep 17 00:00:00 2001 From: Jimmy Jia Date: Sun, 10 May 2015 15:13:17 -0700 Subject: [PATCH] Add context-forwarding trigger factory methods For react-bootstrap/react-bootstrap#465 --- src/ModalTrigger.js | 17 ++++++ src/OverlayTrigger.js | 17 ++++++ src/utils/createContextWrapper.js | 48 +++++++++++++++++ test/ModalTriggerSpec.js | 89 ++++++++++++++++++++----------- test/OverlayTriggerSpec.js | 55 +++++++++++++++---- 5 files changed, 187 insertions(+), 39 deletions(-) create mode 100644 src/utils/createContextWrapper.js diff --git a/src/ModalTrigger.js b/src/ModalTrigger.js index bc227b1f39..8b434f233e 100644 --- a/src/ModalTrigger.js +++ b/src/ModalTrigger.js @@ -2,6 +2,7 @@ import React, { cloneElement } from 'react'; import OverlayMixin from './OverlayMixin'; import createChainedFunction from './utils/createChainedFunction'; +import createContextWrapper from './utils/createContextWrapper'; const ModalTrigger = React.createClass({ mixins: [OverlayMixin], @@ -61,4 +62,20 @@ const ModalTrigger = React.createClass({ } }); +/** + * Creates a new ModalTrigger class that forwards the relevant context + * + * This static method should only be called at the module level, instead of in + * e.g. a render() method, because it's expensive to create new classes. + * + * For example, you would want to have: + * + * > export default ModalTrigger.withContext({ + * > myContextKey: React.PropTypes.object + * > }); + * + * and import this when needed. + */ +ModalTrigger.withContext = createContextWrapper(ModalTrigger, 'modal'); + export default ModalTrigger; diff --git a/src/OverlayTrigger.js b/src/OverlayTrigger.js index 88e01b280b..ec1021cf3c 100644 --- a/src/OverlayTrigger.js +++ b/src/OverlayTrigger.js @@ -4,6 +4,7 @@ import domUtils from './utils/domUtils'; import createChainedFunction from './utils/createChainedFunction'; import assign from './utils/Object.assign'; +import createContextWrapper from './utils/createContextWrapper'; /** * Check if value one is inside or equal to the of value @@ -230,4 +231,20 @@ const OverlayTrigger = React.createClass({ } }); +/** + * Creates a new OverlayTrigger class that forwards the relevant context + * + * This static method should only be called at the module level, instead of in + * e.g. a render() method, because it's expensive to create new classes. + * + * For example, you would want to have: + * + * > export default OverlayTrigger.withContext({ + * > myContextKey: React.PropTypes.object + * > }); + * + * and import this when needed. + */ +OverlayTrigger.withContext = createContextWrapper(OverlayTrigger, 'overlay'); + export default OverlayTrigger; diff --git a/src/utils/createContextWrapper.js b/src/utils/createContextWrapper.js new file mode 100644 index 0000000000..6a5b38082f --- /dev/null +++ b/src/utils/createContextWrapper.js @@ -0,0 +1,48 @@ +import React from 'react'; + +/** + * Creates new trigger class that injects context into overlay. + */ +export default function createContextWrapper(Trigger, propName) { + return function (contextTypes) { + class ContextWrapper extends React.Component { + getChildContext() { + return this.props.context; + } + + render() { + // Strip injected props from below. + const {wrapped, ...props} = this.props; + delete props.context; + + return React.cloneElement(wrapped, props); + } + } + ContextWrapper.childContextTypes = contextTypes; + + class TriggerWithContext { + render() { + const props = {...this.props}; + props[propName] = this.getWrappedOverlay(); + + return ( + + {this.props.children} + + ); + } + + getWrappedOverlay() { + return ( + + ); + } + } + TriggerWithContext.contextTypes = contextTypes; + + return TriggerWithContext; + }; +} diff --git a/test/ModalTriggerSpec.js b/test/ModalTriggerSpec.js index e0c9bc2504..dbdcc8af46 100644 --- a/test/ModalTriggerSpec.js +++ b/test/ModalTriggerSpec.js @@ -4,72 +4,101 @@ import ModalTrigger from '../src/ModalTrigger'; describe('ModalTrigger', function() { it('Should create ModalTrigger element', function() { - let instance = ReactTestUtils.renderIntoDocument( + const instance = ReactTestUtils.renderIntoDocument( test}> ); - let modalTrigger = instance.getDOMNode(); + const modalTrigger = React.findDOMNode(instance); assert.equal(modalTrigger.nodeName, 'BUTTON'); }); it('Should pass ModalTrigger onMouseOver prop to child', function() { - let called = false; - let callback = function() { - called = true; - }; - let instance = ReactTestUtils.renderIntoDocument( + const callback = sinon.spy(); + const instance = ReactTestUtils.renderIntoDocument( test} onMouseOver={callback}> ); - let modalTrigger = instance.getDOMNode(); + const modalTrigger = React.findDOMNode(instance); ReactTestUtils.Simulate.mouseOver(modalTrigger); - assert.equal(called, true); + callback.called.should.be.true; }); it('Should pass ModalTrigger onMouseOut prop to child', function() { - let called = false; - let callback = function() { - called = true; - }; - let instance = ReactTestUtils.renderIntoDocument( + const callback = sinon.spy(); + const instance = ReactTestUtils.renderIntoDocument( test} onMouseOut={callback}> ); - let modalTrigger = instance.getDOMNode(); + const modalTrigger = React.findDOMNode(instance); ReactTestUtils.Simulate.mouseOut(modalTrigger); - assert.equal(called, true); + callback.called.should.be.true; }); it('Should pass ModalTrigger onFocus prop to child', function() { - let called = false; - let callback = function() { - called = true; - }; - let instance = ReactTestUtils.renderIntoDocument( + const callback = sinon.spy(); + const instance = ReactTestUtils.renderIntoDocument( test} onFocus={callback}> ); - let modalTrigger = instance.getDOMNode(); + const modalTrigger = React.findDOMNode(instance); ReactTestUtils.Simulate.focus(modalTrigger); - assert.equal(called, true); + callback.called.should.be.true; }); it('Should pass ModalTrigger onBlur prop to child', function() { - let called = false; - let callback = function() { - called = true; - }; - let instance = ReactTestUtils.renderIntoDocument( + const callback = sinon.spy(); + const instance = ReactTestUtils.renderIntoDocument( test} onBlur={callback}> ); - let modalTrigger = instance.getDOMNode(); + const modalTrigger = React.findDOMNode(instance); ReactTestUtils.Simulate.blur(modalTrigger); - assert.equal(called, true); + callback.called.should.be.true; + }); + + // This is just a copy of the test case for OverlayTrigger. + it('Should forward requested context', function() { + const contextTypes = { + key: React.PropTypes.string + }; + + const contextSpy = sinon.spy(); + class ContextReader extends React.Component { + render() { + contextSpy(this.context.key); + return
; + } + } + ContextReader.contextTypes = contextTypes; + + const TriggerWithContext = ModalTrigger.withContext(contextTypes); + class ContextHolder extends React.Component { + getChildContext() { + return {key: 'value'}; + } + + render() { + return ( + } + > + + + ); + } + } + ContextHolder.childContextTypes = contextTypes; + + const instance = ReactTestUtils.renderIntoDocument(); + const modalTrigger = React.findDOMNode(instance); + ReactTestUtils.Simulate.click(modalTrigger); + + contextSpy.calledWith('value').should.be.true; }); }); diff --git a/test/OverlayTriggerSpec.js b/test/OverlayTriggerSpec.js index e2e358216a..dddf7d6031 100644 --- a/test/OverlayTriggerSpec.js +++ b/test/OverlayTriggerSpec.js @@ -4,27 +4,64 @@ import OverlayTrigger from '../src/OverlayTrigger'; describe('OverlayTrigger', function() { it('Should create OverlayTrigger element', function() { - let instance = ReactTestUtils.renderIntoDocument( + const instance = ReactTestUtils.renderIntoDocument( test
}> ); - let overlayTrigger = instance.getDOMNode(); + const overlayTrigger = React.findDOMNode(instance); assert.equal(overlayTrigger.nodeName, 'BUTTON'); }); it('Should pass OverlayTrigger onClick prop to child', function() { - let called = false; - let callback = function() { - called = true; - }; - let instance = ReactTestUtils.renderIntoDocument( + const callback = sinon.spy(); + const instance = ReactTestUtils.renderIntoDocument( test} onClick={callback}> ); - let overlayTrigger = instance.getDOMNode(); + const overlayTrigger = React.findDOMNode(instance); + ReactTestUtils.Simulate.click(overlayTrigger); + callback.called.should.be.true; + }); + + it('Should forward requested context', function() { + const contextTypes = { + key: React.PropTypes.string + }; + + const contextSpy = sinon.spy(); + class ContextReader extends React.Component { + render() { + contextSpy(this.context.key); + return
; + } + } + ContextReader.contextTypes = contextTypes; + + const TriggerWithContext = OverlayTrigger.withContext(contextTypes); + class ContextHolder extends React.Component { + getChildContext() { + return {key: 'value'}; + } + + render() { + return ( + } + > + + + ); + } + } + ContextHolder.childContextTypes = contextTypes; + + const instance = ReactTestUtils.renderIntoDocument(); + const overlayTrigger = React.findDOMNode(instance); ReactTestUtils.Simulate.click(overlayTrigger); - assert.equal(called, true); + + contextSpy.calledWith('value').should.be.true; }); });