Skip to content

Commit

Permalink
Merge pull request react-bootstrap#698 from taion/overlay-root-close
Browse files Browse the repository at this point in the history
[added] Enable rootClose for OverlayTrigger
  • Loading branch information
taion committed May 19, 2015
2 parents e4d0aff + 5dc0ac2 commit 115412e
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 4 deletions.
18 changes: 18 additions & 0 deletions docs/examples/PopoverTriggerBehaviors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const positionerInstance = (
<ButtonToolbar>
<OverlayTrigger trigger='click' placement='bottom' overlay={<Popover title='Popover bottom'><strong>Holy guacamole!</strong> Check this info.</Popover>}>
<Button bsStyle='default'>Click</Button>
</OverlayTrigger>
<OverlayTrigger trigger='hover' placement='bottom' overlay={<Popover title='Popover bottom'><strong>Holy guacamole!</strong> Check this info.</Popover>}>
<Button bsStyle='default'>Hover</Button>
</OverlayTrigger>
<OverlayTrigger trigger='focus' placement='bottom' overlay={<Popover title='Popover bottom'><strong>Holy guacamole!</strong> Check this info.</Popover>}>
<Button bsStyle='default'>Focus</Button>
</OverlayTrigger>
<OverlayTrigger trigger='click' rootClose={true} placement='bottom' overlay={<Popover title='Popover bottom'><strong>Holy guacamole!</strong> Check this info.</Popover>}>
<Button bsStyle='default'>Click + rootClose</Button>
</OverlayTrigger>
</ButtonToolbar>
);

React.render(positionerInstance, mountNode);
3 changes: 3 additions & 0 deletions docs/src/ComponentsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ const ComponentsPage = React.createClass({
<p>Positioned popover component.</p>
<ReactPlayground codeText={Samples.PopoverPositioned} />

<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>
<ReactPlayground codeText={Samples.PopoverTriggerBehaviors} />

<p>Popover component in container.</p>
<ReactPlayground codeText={Samples.PopoverContained} exampleClassName='bs-example-popover-contained' />

Expand Down
1 change: 1 addition & 0 deletions docs/src/Samples.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default {
TooltipInCopy: require('fs').readFileSync(__dirname + '/../examples/TooltipInCopy.js', 'utf8'),
PopoverBasic: require('fs').readFileSync(__dirname + '/../examples/PopoverBasic.js', 'utf8'),
PopoverPositioned: require('fs').readFileSync(__dirname + '/../examples/PopoverPositioned.js', 'utf8'),
PopoverTriggerBehaviors: require('fs').readFileSync(__dirname + '/../examples/PopoverTriggerBehaviors.js', 'utf8'),
PopoverContained: require('fs').readFileSync(__dirname + '/../examples/PopoverContained.js', 'utf8'),
PopoverPositionedScrolling: require('fs').readFileSync(__dirname + '/../examples/PopoverPositionedScrolling.js', 'utf8'),
ProgressBarBasic: require('fs').readFileSync(__dirname + '/../examples/ProgressBarBasic.js', 'utf8'),
Expand Down
21 changes: 17 additions & 4 deletions src/OverlayTrigger.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { cloneElement } from 'react';

import OverlayMixin from './OverlayMixin';
import domUtils from './utils/domUtils';
import RootCloseWrapper from './RootCloseWrapper';

import createChainedFunction from './utils/createChainedFunction';
import assign from './utils/Object.assign';
import createContextWrapper from './utils/createContextWrapper';
import domUtils from './utils/domUtils';
import assign from './utils/Object.assign';

/**
* Check if value one is inside or equal to the of value
Expand Down Expand Up @@ -34,7 +36,8 @@ const OverlayTrigger = React.createClass({
delayHide: React.PropTypes.number,
defaultOverlayShown: React.PropTypes.bool,
overlay: React.PropTypes.node.isRequired,
containerPadding: React.PropTypes.number
containerPadding: React.PropTypes.number,
rootClose: React.PropTypes.bool
},

getDefaultProps() {
Expand Down Expand Up @@ -83,7 +86,7 @@ const OverlayTrigger = React.createClass({
return <span />;
}

return cloneElement(
const overlay = cloneElement(
this.props.overlay,
{
onRequestHide: this.hide,
Expand All @@ -94,6 +97,16 @@ const OverlayTrigger = React.createClass({
arrowOffsetTop: this.state.arrowOffsetTop
}
);

if (this.props.rootClose) {
return (
<RootCloseWrapper onRootClose={this.hide}>
{overlay}
</RootCloseWrapper>
);
} else {
return overlay;
}
},

render() {
Expand Down
82 changes: 82 additions & 0 deletions src/RootCloseWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';
import domUtils from './utils/domUtils';
import EventListener from './utils/EventListener';

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

/**
* Checks whether a node is within
* a root nodes tree
*
* @param {DOMElement} node
* @param {DOMElement} root
* @returns {boolean}
*/
function isNodeInRoot(node, root) {
while (node) {
if (node === root) {
return true;
}
node = node.parentNode;
}

return false;
}

export default class RootCloseWrapper extends React.Component {
constructor(props) {
super(props);

this.handleDocumentClick = this.handleDocumentClick.bind(this);
this.handleDocumentKeyUp = this.handleDocumentKeyUp.bind(this);
}

bindRootCloseHandlers() {
const doc = domUtils.ownerDocument(this);

this._onDocumentClickListener =
EventListener.listen(doc, 'click', this.handleDocumentClick);
this._onDocumentKeyupListener =
EventListener.listen(doc, 'keyup', this.handleDocumentKeyUp);
}

handleDocumentClick(e) {
// If the click originated from within this component, don't do anything.
if (isNodeInRoot(e.target, React.findDOMNode(this))) {
return;
}

this.props.onRootClose();
}

handleDocumentKeyUp(e) {
if (e.keyCode === 27) {
this.props.onRootClose();
}
}

unbindRootCloseHandlers() {
if (this._onDocumentClickListener) {
this._onDocumentClickListener.remove();
}

if (this._onDocumentKeyupListener) {
this._onDocumentKeyupListener.remove();
}
}

componentDidMount() {
this.bindRootCloseHandlers();
}

render() {
return React.Children.only(this.props.children);
}

componentWillUnmount() {
this.unbindRootCloseHandlers();
}
}
RootCloseWrapper.propTypes = {
onRootClose: React.PropTypes.func.isRequired
};
52 changes: 52 additions & 0 deletions test/OverlayTriggerSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ describe('OverlayTrigger', function() {
callback.called.should.be.true;
});

it('Should show after click trigger', function() {
const instance = ReactTestUtils.renderIntoDocument(
<OverlayTrigger trigger='click' overlay={<div>test</div>}>
<button>button</button>
</OverlayTrigger>
);
const overlayTrigger = React.findDOMNode(instance);
ReactTestUtils.Simulate.click(overlayTrigger);

instance.state.isOverlayShown.should.be.true;
});

it('Should forward requested context', function() {
const contextTypes = {
key: React.PropTypes.string
Expand Down Expand Up @@ -193,4 +205,44 @@ describe('OverlayTrigger', function() {
});
});
});

describe('rootClose', function() {
[
{
label: 'true',
rootClose: true,
shownAfterClick: false
},
{
label: 'default (false)',
rootClose: null,
shownAfterClick: true
}
].forEach(function(testCase) {
describe(testCase.label, function() {
let instance;

beforeEach(function () {
instance = ReactTestUtils.renderIntoDocument(
<OverlayTrigger
overlay={<div>test</div>}
trigger='click' rootClose={testCase.rootClose}
>
<button>button</button>
</OverlayTrigger>
);
const overlayTrigger = React.findDOMNode(instance);
ReactTestUtils.Simulate.click(overlayTrigger);
});

it('Should have correct isOverlayShown state', function () {
const event = document.createEvent('HTMLEvents');
event.initEvent('click', true, true);
document.documentElement.dispatchEvent(event);

instance.state.isOverlayShown.should.equal(testCase.shownAfterClick);
});
});
});
});
});

0 comments on commit 115412e

Please sign in to comment.