Skip to content

Commit 70c6efd

Browse files
committed
Merge pull request react-bootstrap#644 from taion/overlay-context
Add context-forwarding trigger factory methods
2 parents 959e44f + 2b03b1a commit 70c6efd

5 files changed

Lines changed: 187 additions & 39 deletions

File tree

src/ModalTrigger.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { cloneElement } from 'react';
22
import OverlayMixin from './OverlayMixin';
33

44
import createChainedFunction from './utils/createChainedFunction';
5+
import createContextWrapper from './utils/createContextWrapper';
56

67
const ModalTrigger = React.createClass({
78
mixins: [OverlayMixin],
@@ -61,4 +62,20 @@ const ModalTrigger = React.createClass({
6162
}
6263
});
6364

65+
/**
66+
* Creates a new ModalTrigger class that forwards the relevant context
67+
*
68+
* This static method should only be called at the module level, instead of in
69+
* e.g. a render() method, because it's expensive to create new classes.
70+
*
71+
* For example, you would want to have:
72+
*
73+
* > export default ModalTrigger.withContext({
74+
* > myContextKey: React.PropTypes.object
75+
* > });
76+
*
77+
* and import this when needed.
78+
*/
79+
ModalTrigger.withContext = createContextWrapper(ModalTrigger, 'modal');
80+
6481
export default ModalTrigger;

src/OverlayTrigger.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import domUtils from './utils/domUtils';
44

55
import createChainedFunction from './utils/createChainedFunction';
66
import assign from './utils/Object.assign';
7+
import createContextWrapper from './utils/createContextWrapper';
78

89
/**
910
* Check if value one is inside or equal to the of value
@@ -230,4 +231,20 @@ const OverlayTrigger = React.createClass({
230231
}
231232
});
232233

234+
/**
235+
* Creates a new OverlayTrigger class that forwards the relevant context
236+
*
237+
* This static method should only be called at the module level, instead of in
238+
* e.g. a render() method, because it's expensive to create new classes.
239+
*
240+
* For example, you would want to have:
241+
*
242+
* > export default OverlayTrigger.withContext({
243+
* > myContextKey: React.PropTypes.object
244+
* > });
245+
*
246+
* and import this when needed.
247+
*/
248+
OverlayTrigger.withContext = createContextWrapper(OverlayTrigger, 'overlay');
249+
233250
export default OverlayTrigger;

src/utils/createContextWrapper.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from 'react';
2+
3+
/**
4+
* Creates new trigger class that injects context into overlay.
5+
*/
6+
export default function createContextWrapper(Trigger, propName) {
7+
return function (contextTypes) {
8+
class ContextWrapper extends React.Component {
9+
getChildContext() {
10+
return this.props.context;
11+
}
12+
13+
render() {
14+
// Strip injected props from below.
15+
const {wrapped, ...props} = this.props;
16+
delete props.context;
17+
18+
return React.cloneElement(wrapped, props);
19+
}
20+
}
21+
ContextWrapper.childContextTypes = contextTypes;
22+
23+
class TriggerWithContext {
24+
render() {
25+
const props = {...this.props};
26+
props[propName] = this.getWrappedOverlay();
27+
28+
return (
29+
<Trigger {...props}>
30+
{this.props.children}
31+
</Trigger>
32+
);
33+
}
34+
35+
getWrappedOverlay() {
36+
return (
37+
<ContextWrapper
38+
context={this.context}
39+
wrapped={this.props[propName]}
40+
/>
41+
);
42+
}
43+
}
44+
TriggerWithContext.contextTypes = contextTypes;
45+
46+
return TriggerWithContext;
47+
};
48+
}

test/ModalTriggerSpec.js

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,72 +4,101 @@ import ModalTrigger from '../src/ModalTrigger';
44

55
describe('ModalTrigger', function() {
66
it('Should create ModalTrigger element', function() {
7-
let instance = ReactTestUtils.renderIntoDocument(
7+
const instance = ReactTestUtils.renderIntoDocument(
88
<ModalTrigger modal={<div>test</div>}>
99
<button>button</button>
1010
</ModalTrigger>
1111
);
12-
let modalTrigger = instance.getDOMNode();
12+
const modalTrigger = React.findDOMNode(instance);
1313
assert.equal(modalTrigger.nodeName, 'BUTTON');
1414
});
1515

1616
it('Should pass ModalTrigger onMouseOver prop to child', function() {
17-
let called = false;
18-
let callback = function() {
19-
called = true;
20-
};
21-
let instance = ReactTestUtils.renderIntoDocument(
17+
const callback = sinon.spy();
18+
const instance = ReactTestUtils.renderIntoDocument(
2219
<ModalTrigger modal={<div>test</div>} onMouseOver={callback}>
2320
<button>button</button>
2421
</ModalTrigger>
2522
);
26-
let modalTrigger = instance.getDOMNode();
23+
const modalTrigger = React.findDOMNode(instance);
2724
ReactTestUtils.Simulate.mouseOver(modalTrigger);
28-
assert.equal(called, true);
25+
callback.called.should.be.true;
2926
});
3027

3128
it('Should pass ModalTrigger onMouseOut prop to child', function() {
32-
let called = false;
33-
let callback = function() {
34-
called = true;
35-
};
36-
let instance = ReactTestUtils.renderIntoDocument(
29+
const callback = sinon.spy();
30+
const instance = ReactTestUtils.renderIntoDocument(
3731
<ModalTrigger modal={<div>test</div>} onMouseOut={callback}>
3832
<button>button</button>
3933
</ModalTrigger>
4034
);
41-
let modalTrigger = instance.getDOMNode();
35+
const modalTrigger = React.findDOMNode(instance);
4236
ReactTestUtils.Simulate.mouseOut(modalTrigger);
43-
assert.equal(called, true);
37+
callback.called.should.be.true;
4438
});
4539

4640
it('Should pass ModalTrigger onFocus prop to child', function() {
47-
let called = false;
48-
let callback = function() {
49-
called = true;
50-
};
51-
let instance = ReactTestUtils.renderIntoDocument(
41+
const callback = sinon.spy();
42+
const instance = ReactTestUtils.renderIntoDocument(
5243
<ModalTrigger modal={<div>test</div>} onFocus={callback}>
5344
<button>button</button>
5445
</ModalTrigger>
5546
);
56-
let modalTrigger = instance.getDOMNode();
47+
const modalTrigger = React.findDOMNode(instance);
5748
ReactTestUtils.Simulate.focus(modalTrigger);
58-
assert.equal(called, true);
49+
callback.called.should.be.true;
5950
});
6051

6152
it('Should pass ModalTrigger onBlur prop to child', function() {
62-
let called = false;
63-
let callback = function() {
64-
called = true;
65-
};
66-
let instance = ReactTestUtils.renderIntoDocument(
53+
const callback = sinon.spy();
54+
const instance = ReactTestUtils.renderIntoDocument(
6755
<ModalTrigger modal={<div>test</div>} onBlur={callback}>
6856
<button>button</button>
6957
</ModalTrigger>
7058
);
71-
let modalTrigger = instance.getDOMNode();
59+
const modalTrigger = React.findDOMNode(instance);
7260
ReactTestUtils.Simulate.blur(modalTrigger);
73-
assert.equal(called, true);
61+
callback.called.should.be.true;
62+
});
63+
64+
// This is just a copy of the test case for OverlayTrigger.
65+
it('Should forward requested context', function() {
66+
const contextTypes = {
67+
key: React.PropTypes.string
68+
};
69+
70+
const contextSpy = sinon.spy();
71+
class ContextReader extends React.Component {
72+
render() {
73+
contextSpy(this.context.key);
74+
return <div />;
75+
}
76+
}
77+
ContextReader.contextTypes = contextTypes;
78+
79+
const TriggerWithContext = ModalTrigger.withContext(contextTypes);
80+
class ContextHolder extends React.Component {
81+
getChildContext() {
82+
return {key: 'value'};
83+
}
84+
85+
render() {
86+
return (
87+
<TriggerWithContext
88+
trigger="click"
89+
modal={<ContextReader />}
90+
>
91+
<button>button</button>
92+
</TriggerWithContext>
93+
);
94+
}
95+
}
96+
ContextHolder.childContextTypes = contextTypes;
97+
98+
const instance = ReactTestUtils.renderIntoDocument(<ContextHolder />);
99+
const modalTrigger = React.findDOMNode(instance);
100+
ReactTestUtils.Simulate.click(modalTrigger);
101+
102+
contextSpy.calledWith('value').should.be.true;
74103
});
75104
});

test/OverlayTriggerSpec.js

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,64 @@ import OverlayTrigger from '../src/OverlayTrigger';
44

55
describe('OverlayTrigger', function() {
66
it('Should create OverlayTrigger element', function() {
7-
let instance = ReactTestUtils.renderIntoDocument(
7+
const instance = ReactTestUtils.renderIntoDocument(
88
<OverlayTrigger overlay={<div>test</div>}>
99
<button>button</button>
1010
</OverlayTrigger>
1111
);
12-
let overlayTrigger = instance.getDOMNode();
12+
const overlayTrigger = React.findDOMNode(instance);
1313
assert.equal(overlayTrigger.nodeName, 'BUTTON');
1414
});
1515

1616
it('Should pass OverlayTrigger onClick prop to child', function() {
17-
let called = false;
18-
let callback = function() {
19-
called = true;
20-
};
21-
let instance = ReactTestUtils.renderIntoDocument(
17+
const callback = sinon.spy();
18+
const instance = ReactTestUtils.renderIntoDocument(
2219
<OverlayTrigger overlay={<div>test</div>} onClick={callback}>
2320
<button>button</button>
2421
</OverlayTrigger>
2522
);
26-
let overlayTrigger = instance.getDOMNode();
23+
const overlayTrigger = React.findDOMNode(instance);
24+
ReactTestUtils.Simulate.click(overlayTrigger);
25+
callback.called.should.be.true;
26+
});
27+
28+
it('Should forward requested context', function() {
29+
const contextTypes = {
30+
key: React.PropTypes.string
31+
};
32+
33+
const contextSpy = sinon.spy();
34+
class ContextReader extends React.Component {
35+
render() {
36+
contextSpy(this.context.key);
37+
return <div />;
38+
}
39+
}
40+
ContextReader.contextTypes = contextTypes;
41+
42+
const TriggerWithContext = OverlayTrigger.withContext(contextTypes);
43+
class ContextHolder extends React.Component {
44+
getChildContext() {
45+
return {key: 'value'};
46+
}
47+
48+
render() {
49+
return (
50+
<TriggerWithContext
51+
trigger="click"
52+
overlay={<ContextReader />}
53+
>
54+
<button>button</button>
55+
</TriggerWithContext>
56+
);
57+
}
58+
}
59+
ContextHolder.childContextTypes = contextTypes;
60+
61+
const instance = ReactTestUtils.renderIntoDocument(<ContextHolder />);
62+
const overlayTrigger = React.findDOMNode(instance);
2763
ReactTestUtils.Simulate.click(overlayTrigger);
28-
assert.equal(called, true);
64+
65+
contextSpy.calledWith('value').should.be.true;
2966
});
3067
});

0 commit comments

Comments
 (0)