Skip to content

Commit

Permalink
Merge pull request react-bootstrap#644 from taion/overlay-context
Browse files Browse the repository at this point in the history
Add context-forwarding trigger factory methods
  • Loading branch information
taion committed May 11, 2015
2 parents 959e44f + 2b03b1a commit 70c6efd
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 39 deletions.
17 changes: 17 additions & 0 deletions src/ModalTrigger.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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;
17 changes: 17 additions & 0 deletions src/OverlayTrigger.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
48 changes: 48 additions & 0 deletions src/utils/createContextWrapper.js
Original file line number Diff line number Diff line change
@@ -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 (
<Trigger {...props}>
{this.props.children}
</Trigger>
);
}

getWrappedOverlay() {
return (
<ContextWrapper
context={this.context}
wrapped={this.props[propName]}
/>
);
}
}
TriggerWithContext.contextTypes = contextTypes;

return TriggerWithContext;
};
}
89 changes: 59 additions & 30 deletions test/ModalTriggerSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ModalTrigger modal={<div>test</div>}>
<button>button</button>
</ModalTrigger>
);
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(
<ModalTrigger modal={<div>test</div>} onMouseOver={callback}>
<button>button</button>
</ModalTrigger>
);
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(
<ModalTrigger modal={<div>test</div>} onMouseOut={callback}>
<button>button</button>
</ModalTrigger>
);
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(
<ModalTrigger modal={<div>test</div>} onFocus={callback}>
<button>button</button>
</ModalTrigger>
);
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(
<ModalTrigger modal={<div>test</div>} onBlur={callback}>
<button>button</button>
</ModalTrigger>
);
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 <div />;
}
}
ContextReader.contextTypes = contextTypes;

const TriggerWithContext = ModalTrigger.withContext(contextTypes);
class ContextHolder extends React.Component {
getChildContext() {
return {key: 'value'};
}

render() {
return (
<TriggerWithContext
trigger="click"
modal={<ContextReader />}
>
<button>button</button>
</TriggerWithContext>
);
}
}
ContextHolder.childContextTypes = contextTypes;

const instance = ReactTestUtils.renderIntoDocument(<ContextHolder />);
const modalTrigger = React.findDOMNode(instance);
ReactTestUtils.Simulate.click(modalTrigger);

contextSpy.calledWith('value').should.be.true;
});
});
55 changes: 46 additions & 9 deletions test/OverlayTriggerSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<OverlayTrigger overlay={<div>test</div>}>
<button>button</button>
</OverlayTrigger>
);
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(
<OverlayTrigger overlay={<div>test</div>} onClick={callback}>
<button>button</button>
</OverlayTrigger>
);
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 <div />;
}
}
ContextReader.contextTypes = contextTypes;

const TriggerWithContext = OverlayTrigger.withContext(contextTypes);
class ContextHolder extends React.Component {
getChildContext() {
return {key: 'value'};
}

render() {
return (
<TriggerWithContext
trigger="click"
overlay={<ContextReader />}
>
<button>button</button>
</TriggerWithContext>
);
}
}
ContextHolder.childContextTypes = contextTypes;

const instance = ReactTestUtils.renderIntoDocument(<ContextHolder />);
const overlayTrigger = React.findDOMNode(instance);
ReactTestUtils.Simulate.click(overlayTrigger);
assert.equal(called, true);

contextSpy.calledWith('value').should.be.true;
});
});

0 comments on commit 70c6efd

Please sign in to comment.