Skip to content

Commit b67081b

Browse files
author
Jimmy Jia
committed
[fixed] rootClose behavior on replaced elements
Fixes react-bootstrap#802
1 parent 62181b3 commit b67081b

File tree

3 files changed

+88
-10
lines changed

3 files changed

+88
-10
lines changed

src/OverlayMixin.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default {
3737

3838
let overlay = this.renderOverlay();
3939

40-
// Save reference to help testing
40+
// Save reference for future access.
4141
if (overlay !== null) {
4242
this._overlayInstance = React.render(overlay, this._overlayTarget);
4343
} else {
@@ -57,7 +57,11 @@ export default {
5757
}
5858

5959
if (this._overlayInstance) {
60-
return React.findDOMNode(this._overlayInstance);
60+
if (this._overlayInstance.getWrappedDOMNode) {
61+
return this._overlayInstance.getWrappedDOMNode();
62+
} else {
63+
return React.findDOMNode(this._overlayInstance);
64+
}
6165
}
6266

6367
return null;

src/RootCloseWrapper.js

+26-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import EventListener from './utils/EventListener';
44

55
// TODO: Merge this logic with dropdown logic once #526 is done.
66

7+
// TODO: Consider using an ES6 symbol here, once we use babel-runtime.
8+
const CLICK_WAS_INSIDE = '__click_was_inside';
9+
10+
function suppressRootClose(event) {
11+
// Tag the native event to prevent the root close logic on document click.
12+
// This seems safer than using event.nativeEvent.stopImmediatePropagation(),
13+
// which is only supported in IE >= 9.
14+
event.nativeEvent[CLICK_WAS_INSIDE] = true;
15+
}
716

817
export default class RootCloseWrapper extends React.Component {
918
constructor(props) {
@@ -23,10 +32,8 @@ export default class RootCloseWrapper extends React.Component {
2332
}
2433

2534
handleDocumentClick(e) {
26-
// If the click originated from within this component, don't do anything.
27-
// e.srcElement is required for IE8 as e.target is undefined
28-
let target = e.target || e.srcElement;
29-
if (domUtils.contains(React.findDOMNode(this), target)) {
35+
// This is now the native event.
36+
if (e[CLICK_WAS_INSIDE]) {
3037
return;
3138
}
3239

@@ -54,7 +61,21 @@ export default class RootCloseWrapper extends React.Component {
5461
}
5562

5663
render() {
57-
return React.Children.only(this.props.children);
64+
// Wrap the child in a new element, so the child won't have to handle
65+
// potentially combining multiple onClick listeners.
66+
return (
67+
<div onClick={suppressRootClose}>
68+
{React.Children.only(this.props.children)}
69+
</div>
70+
);
71+
}
72+
73+
getWrappedDOMNode() {
74+
// We can't use a ref to identify the wrapped child, since we might be
75+
// stealing the ref from the owner, but we know exactly the DOM structure
76+
// that will be rendered, so we can just do this to get the child's DOM
77+
// node for doing size calculations in OverlayMixin.
78+
return React.findDOMNode(this).children[0];
5879
}
5980

6081
componentWillUnmount() {

test/OverlayTriggerSpec.js

+56-3
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,66 @@ describe('OverlayTrigger', function() {
236236
});
237237

238238
it('Should have correct isOverlayShown state', function () {
239-
const event = document.createEvent('HTMLEvents');
240-
event.initEvent('click', true, true);
241-
document.documentElement.dispatchEvent(event);
239+
document.documentElement.click();
242240

241+
// Need to click this way for it to propagate to document element.
243242
instance.state.isOverlayShown.should.equal(testCase.shownAfterClick);
244243
});
245244
});
246245
});
246+
247+
describe('replaced overlay', function () {
248+
let instance;
249+
250+
beforeEach(function () {
251+
class ReplacedOverlay extends React.Component {
252+
constructor(props) {
253+
super(props);
254+
255+
this.handleClick = this.handleClick.bind(this);
256+
this.state = {replaced: false};
257+
}
258+
259+
handleClick() {
260+
this.setState({replaced: true});
261+
}
262+
263+
render() {
264+
if (this.state.replaced) {
265+
return (
266+
<div>replaced</div>
267+
);
268+
} else {
269+
return (
270+
<div>
271+
<a id="replace-overlay" onClick={this.handleClick}>
272+
original
273+
</a>
274+
</div>
275+
);
276+
}
277+
}
278+
}
279+
280+
instance = ReactTestUtils.renderIntoDocument(
281+
<OverlayTrigger
282+
overlay={<ReplacedOverlay />}
283+
trigger='click' rootClose={true}
284+
>
285+
<button>button</button>
286+
</OverlayTrigger>
287+
);
288+
const overlayTrigger = React.findDOMNode(instance);
289+
ReactTestUtils.Simulate.click(overlayTrigger);
290+
});
291+
292+
it('Should still be shown', function () {
293+
// Need to click this way for it to propagate to document element.
294+
const replaceOverlay = document.getElementById('replace-overlay');
295+
replaceOverlay.click();
296+
297+
instance.state.isOverlayShown.should.be.true;
298+
});
299+
});
247300
});
248301
});

0 commit comments

Comments
 (0)