Skip to content

Commit 5dc0ac2

Browse files
author
Jimmy Jia
committed
[added] Enable rootClose for OverlayTrigger
Fixes react-bootstrap#233
1 parent e4d0aff commit 5dc0ac2

6 files changed

+173
-4
lines changed
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const positionerInstance = (
2+
<ButtonToolbar>
3+
<OverlayTrigger trigger='click' placement='bottom' overlay={<Popover title='Popover bottom'><strong>Holy guacamole!</strong> Check this info.</Popover>}>
4+
<Button bsStyle='default'>Click</Button>
5+
</OverlayTrigger>
6+
<OverlayTrigger trigger='hover' placement='bottom' overlay={<Popover title='Popover bottom'><strong>Holy guacamole!</strong> Check this info.</Popover>}>
7+
<Button bsStyle='default'>Hover</Button>
8+
</OverlayTrigger>
9+
<OverlayTrigger trigger='focus' placement='bottom' overlay={<Popover title='Popover bottom'><strong>Holy guacamole!</strong> Check this info.</Popover>}>
10+
<Button bsStyle='default'>Focus</Button>
11+
</OverlayTrigger>
12+
<OverlayTrigger trigger='click' rootClose={true} placement='bottom' overlay={<Popover title='Popover bottom'><strong>Holy guacamole!</strong> Check this info.</Popover>}>
13+
<Button bsStyle='default'>Click + rootClose</Button>
14+
</OverlayTrigger>
15+
</ButtonToolbar>
16+
);
17+
18+
React.render(positionerInstance, mountNode);

docs/src/ComponentsPage.js

+3
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,9 @@ const ComponentsPage = React.createClass({
272272
<p>Positioned popover component.</p>
273273
<ReactPlayground codeText={Samples.PopoverPositioned} />
274274

275+
<p>Trigger behaviors. It's inadvisable to use <code>"hover"</code> or <code>"focus"</code> triggers for popovers, because they have poor accessibility from keyboard and on mobile devices.</p>
276+
<ReactPlayground codeText={Samples.PopoverTriggerBehaviors} />
277+
275278
<p>Popover component in container.</p>
276279
<ReactPlayground codeText={Samples.PopoverContained} exampleClassName='bs-example-popover-contained' />
277280

docs/src/Samples.js

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default {
4040
TooltipInCopy: require('fs').readFileSync(__dirname + '/../examples/TooltipInCopy.js', 'utf8'),
4141
PopoverBasic: require('fs').readFileSync(__dirname + '/../examples/PopoverBasic.js', 'utf8'),
4242
PopoverPositioned: require('fs').readFileSync(__dirname + '/../examples/PopoverPositioned.js', 'utf8'),
43+
PopoverTriggerBehaviors: require('fs').readFileSync(__dirname + '/../examples/PopoverTriggerBehaviors.js', 'utf8'),
4344
PopoverContained: require('fs').readFileSync(__dirname + '/../examples/PopoverContained.js', 'utf8'),
4445
PopoverPositionedScrolling: require('fs').readFileSync(__dirname + '/../examples/PopoverPositionedScrolling.js', 'utf8'),
4546
ProgressBarBasic: require('fs').readFileSync(__dirname + '/../examples/ProgressBarBasic.js', 'utf8'),

src/OverlayTrigger.js

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import React, { cloneElement } from 'react';
2+
23
import OverlayMixin from './OverlayMixin';
3-
import domUtils from './utils/domUtils';
4+
import RootCloseWrapper from './RootCloseWrapper';
45

56
import createChainedFunction from './utils/createChainedFunction';
6-
import assign from './utils/Object.assign';
77
import createContextWrapper from './utils/createContextWrapper';
8+
import domUtils from './utils/domUtils';
9+
import assign from './utils/Object.assign';
810

911
/**
1012
* Check if value one is inside or equal to the of value
@@ -34,7 +36,8 @@ const OverlayTrigger = React.createClass({
3436
delayHide: React.PropTypes.number,
3537
defaultOverlayShown: React.PropTypes.bool,
3638
overlay: React.PropTypes.node.isRequired,
37-
containerPadding: React.PropTypes.number
39+
containerPadding: React.PropTypes.number,
40+
rootClose: React.PropTypes.bool
3841
},
3942

4043
getDefaultProps() {
@@ -83,7 +86,7 @@ const OverlayTrigger = React.createClass({
8386
return <span />;
8487
}
8588

86-
return cloneElement(
89+
const overlay = cloneElement(
8790
this.props.overlay,
8891
{
8992
onRequestHide: this.hide,
@@ -94,6 +97,16 @@ const OverlayTrigger = React.createClass({
9497
arrowOffsetTop: this.state.arrowOffsetTop
9598
}
9699
);
100+
101+
if (this.props.rootClose) {
102+
return (
103+
<RootCloseWrapper onRootClose={this.hide}>
104+
{overlay}
105+
</RootCloseWrapper>
106+
);
107+
} else {
108+
return overlay;
109+
}
97110
},
98111

99112
render() {

src/RootCloseWrapper.js

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from 'react';
2+
import domUtils from './utils/domUtils';
3+
import EventListener from './utils/EventListener';
4+
5+
// TODO: Merge this logic with dropdown logic once #526 is done.
6+
7+
/**
8+
* Checks whether a node is within
9+
* a root nodes tree
10+
*
11+
* @param {DOMElement} node
12+
* @param {DOMElement} root
13+
* @returns {boolean}
14+
*/
15+
function isNodeInRoot(node, root) {
16+
while (node) {
17+
if (node === root) {
18+
return true;
19+
}
20+
node = node.parentNode;
21+
}
22+
23+
return false;
24+
}
25+
26+
export default class RootCloseWrapper extends React.Component {
27+
constructor(props) {
28+
super(props);
29+
30+
this.handleDocumentClick = this.handleDocumentClick.bind(this);
31+
this.handleDocumentKeyUp = this.handleDocumentKeyUp.bind(this);
32+
}
33+
34+
bindRootCloseHandlers() {
35+
const doc = domUtils.ownerDocument(this);
36+
37+
this._onDocumentClickListener =
38+
EventListener.listen(doc, 'click', this.handleDocumentClick);
39+
this._onDocumentKeyupListener =
40+
EventListener.listen(doc, 'keyup', this.handleDocumentKeyUp);
41+
}
42+
43+
handleDocumentClick(e) {
44+
// If the click originated from within this component, don't do anything.
45+
if (isNodeInRoot(e.target, React.findDOMNode(this))) {
46+
return;
47+
}
48+
49+
this.props.onRootClose();
50+
}
51+
52+
handleDocumentKeyUp(e) {
53+
if (e.keyCode === 27) {
54+
this.props.onRootClose();
55+
}
56+
}
57+
58+
unbindRootCloseHandlers() {
59+
if (this._onDocumentClickListener) {
60+
this._onDocumentClickListener.remove();
61+
}
62+
63+
if (this._onDocumentKeyupListener) {
64+
this._onDocumentKeyupListener.remove();
65+
}
66+
}
67+
68+
componentDidMount() {
69+
this.bindRootCloseHandlers();
70+
}
71+
72+
render() {
73+
return React.Children.only(this.props.children);
74+
}
75+
76+
componentWillUnmount() {
77+
this.unbindRootCloseHandlers();
78+
}
79+
}
80+
RootCloseWrapper.propTypes = {
81+
onRootClose: React.PropTypes.func.isRequired
82+
};

test/OverlayTriggerSpec.js

+52
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ describe('OverlayTrigger', function() {
2727
callback.called.should.be.true;
2828
});
2929

30+
it('Should show after click trigger', function() {
31+
const instance = ReactTestUtils.renderIntoDocument(
32+
<OverlayTrigger trigger='click' overlay={<div>test</div>}>
33+
<button>button</button>
34+
</OverlayTrigger>
35+
);
36+
const overlayTrigger = React.findDOMNode(instance);
37+
ReactTestUtils.Simulate.click(overlayTrigger);
38+
39+
instance.state.isOverlayShown.should.be.true;
40+
});
41+
3042
it('Should forward requested context', function() {
3143
const contextTypes = {
3244
key: React.PropTypes.string
@@ -193,4 +205,44 @@ describe('OverlayTrigger', function() {
193205
});
194206
});
195207
});
208+
209+
describe('rootClose', function() {
210+
[
211+
{
212+
label: 'true',
213+
rootClose: true,
214+
shownAfterClick: false
215+
},
216+
{
217+
label: 'default (false)',
218+
rootClose: null,
219+
shownAfterClick: true
220+
}
221+
].forEach(function(testCase) {
222+
describe(testCase.label, function() {
223+
let instance;
224+
225+
beforeEach(function () {
226+
instance = ReactTestUtils.renderIntoDocument(
227+
<OverlayTrigger
228+
overlay={<div>test</div>}
229+
trigger='click' rootClose={testCase.rootClose}
230+
>
231+
<button>button</button>
232+
</OverlayTrigger>
233+
);
234+
const overlayTrigger = React.findDOMNode(instance);
235+
ReactTestUtils.Simulate.click(overlayTrigger);
236+
});
237+
238+
it('Should have correct isOverlayShown state', function () {
239+
const event = document.createEvent('HTMLEvents');
240+
event.initEvent('click', true, true);
241+
document.documentElement.dispatchEvent(event);
242+
243+
instance.state.isOverlayShown.should.equal(testCase.shownAfterClick);
244+
});
245+
});
246+
});
247+
});
196248
});

0 commit comments

Comments
 (0)