diff --git a/docs/examples/OverlayTrigger.js b/docs/examples/OverlayTrigger.js
new file mode 100644
index 0000000000..667c6ce2d2
--- /dev/null
+++ b/docs/examples/OverlayTrigger.js
@@ -0,0 +1,35 @@
+
+const Example = React.createClass({
+ render(){
+ const style = {
+ position: 'absolute',
+ backgroundColor: '#EEE',
+ border: '1px solid #CCC',
+ borderRadius: 3,
+ marginLeft: 5,
+ padding: 10
+ };
+
+ const overlay = (
+
+ Holy guacamole! Check this info.
+
+ );
+
+ return (
+
+
+ I am an Overlay target (click me)
+
+
+ );
+ }
+});
+
+React.render( , mountNode);
diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js
index a04a1097c7..3ac37f435e 100644
--- a/docs/src/ComponentsPage.js
+++ b/docs/src/ComponentsPage.js
@@ -306,49 +306,79 @@ const ComponentsPage = React.createClass({
- {/* Tooltip */}
+ {/* overlays */}
-
-
+
Overlays Overlay, Tooltip, Popover
-
Tooltip component.
-
+
Overlay
+
+ Overlays allow components to be rendered and positioned to the left, right, top, or bottom of another component.
+ They are perfect for simple tooltips or even more complicated popups.
+
+
-
Positioned tooltip component.
-
+
Overlay Trigger
+
+ Often you will want to show or hide and Overlay in response to an action by its target, such as hovering over a link.
+ Since this is such a common pattern we provide the OverlayTrigger
component to reduce the amount of boilerplate
+ you need to write to implement this pattern.
+
+
-
Positioned tooltip in copy.
-
+
Props
-
+
Overlay
+
-
+
OverlayTrigger
+
-
+ {/* Tooltip */}
+
+
+
+ You don't always need to create custom styling for your overlays. Bootstrap provides two great options out of the box.
+ Tooltips can be used inside an Overlay
Component, or an OverlayTrigger
+
+
- {/* Popover */}
-
-
Popovers Popover
-
Example popovers
+
Positioned tooltip component.
+
-
Popover component.
-
+
Positioned tooltip in copy.
+
-
Positioned popover component.
-
+
+
-
Trigger behaviors. It's inadvisable to use "hover"
or "focus"
triggers for popovers, because they have poor accessibility from keyboard and on mobile devices.
-
+
+
+ {/* Popover */}
+
+
Popovers
+
+
+ The Popover component, like the Tooltip can be used with an Overlay
Component, or an OverlayTrigger
.
+ Unlike the Tooltip popovers are designed to display more reobust information about their targets.
+
+
- Popover component in container.
-
+ Positioned popover component.
+
- Positioned popover components in scrolling container.
-
+ Trigger behaviors. It's inadvisable to use "hover"
or "focus"
triggers for popovers, because they have poor accessibility from keyboard and on mobile devices.
+
- Props
+ Popover component in container.
+
-
+ Positioned popover components in scrolling container.
+
+
+ Props
+
+
+
{/* Progress Bar */}
@@ -833,6 +863,7 @@ const ComponentsPage = React.createClass({
Panels
Modals
+ Overlays
Tooltips
Popovers
Progress bars
diff --git a/src/OverlayTrigger.js b/src/OverlayTrigger.js
index b708faf0bd..bb22fd88ae 100644
--- a/src/OverlayTrigger.js
+++ b/src/OverlayTrigger.js
@@ -1,11 +1,13 @@
import React, { cloneElement } from 'react';
-import OverlayMixin from './OverlayMixin';
-import RootCloseWrapper from './RootCloseWrapper';
-
import createChainedFunction from './utils/createChainedFunction';
import createContextWrapper from './utils/createContextWrapper';
-import domUtils from './utils/domUtils';
+import CustomPropTypes from './utils/CustomPropTypes';
+import Overlay from './Overlay';
+import position from './utils/overlayPositionUtils';
+
+import deprecationWarning from './utils/deprecationWarning';
+import warning from 'react/lib/warning';
/**
* Check if value one is inside or equal to the of value
@@ -22,52 +24,84 @@ function isOneOf(one, of) {
}
const OverlayTrigger = React.createClass({
- mixins: [OverlayMixin],
propTypes: {
+ /**
+ * The event or action that toggles the overlay visibility.
+ */
trigger: React.PropTypes.oneOfType([
React.PropTypes.oneOf(['manual', 'click', 'hover', 'focus']),
React.PropTypes.arrayOf(React.PropTypes.oneOf(['click', 'hover', 'focus']))
]),
- placement: React.PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
+
+ /**
+ * A millisecond delay amount to show and hide the Overlay once triggered
+ */
delay: React.PropTypes.number,
+ /**
+ * A millisecond delay amount before showing the Overlay once triggered.
+ */
delayShow: React.PropTypes.number,
+ /**
+ * A millisecond delay amount before hiding the Overlay once triggered.
+ */
delayHide: React.PropTypes.number,
+
+ /**
+ * The initial visibility state of the Overlay, for more nuanced visibility controll consider
+ * using the Overlay component directly.
+ */
defaultOverlayShown: React.PropTypes.bool,
+
+ /**
+ * An element or text to overlay next to the target.
+ */
overlay: React.PropTypes.node.isRequired,
+ /**
+ * @private
+ */
onBlur: React.PropTypes.func,
+ /**
+ * @private
+ */
onClick: React.PropTypes.func,
+ /**
+ * @private
+ */
onFocus: React.PropTypes.func,
+ /**
+ * @private
+ */
onMouseEnter: React.PropTypes.func,
+ /**
+ * @private
+ */
onMouseLeave: React.PropTypes.func,
+
+ container: CustomPropTypes.mountable,
+
containerPadding: React.PropTypes.number,
+ placement: React.PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
+
rootClose: React.PropTypes.bool
},
getDefaultProps() {
return {
- placement: 'right',
- trigger: ['hover', 'focus'],
- containerPadding: 0
+ trigger: ['hover', 'focus']
};
},
getInitialState() {
return {
isOverlayShown: this.props.defaultOverlayShown == null ?
- false : this.props.defaultOverlayShown,
- overlayLeft: null,
- overlayTop: null,
- arrowOffsetLeft: null,
- arrowOffsetTop: null
+ false : this.props.defaultOverlayShown
};
},
show() {
this.setState({
isOverlayShown: true
- }, function() {
- this.updateOverlayPosition();
});
},
@@ -81,77 +115,90 @@ const OverlayTrigger = React.createClass({
if (this.state.isOverlayShown) {
this.hide();
} else {
- this.show();
+ this.show();
}
},
- renderOverlay() {
- if (!this.state.isOverlayShown) {
- return ;
- }
+ componentDidMount(){
+ this._mountNode = document.createElement('div');
+ React.render(this._overlay, this._mountNode);
+ },
- const overlay = cloneElement(
- this.props.overlay,
- {
- onRequestHide: this.hide,
- placement: this.props.placement,
- positionLeft: this.state.overlayLeft,
- positionTop: this.state.overlayTop,
- arrowOffsetLeft: this.state.arrowOffsetLeft,
- arrowOffsetTop: this.state.arrowOffsetTop
- }
- );
+ componentWillUnmount() {
+ React.unmountComponentAtNode(this._mountNode);
+ this._mountNode = null;
+ clearTimeout(this._hoverDelay);
+ },
- if (this.props.rootClose) {
- return (
-
- {overlay}
-
- );
- } else {
- return overlay;
- }
+ componentDidUpdate(){
+ React.render(this._overlay, this._mountNode);
+ },
+
+ getOverlay(){
+ let props = {
+ show: this.state.isOverlayShown,
+ onHide: this.hide,
+ rootClose: this.props.rootClose,
+ target: ()=> React.findDOMNode(this),
+ placement: this.props.placement,
+ container: this.props.container,
+ containerPadding: this.props.containerPadding
+ };
+
+ let overlay = cloneElement(this.props.overlay, {
+ placement: props.placement,
+ container: props.container
+ });
+
+ return (
+
+ { overlay }
+
+ );
},
render() {
- const child = React.Children.only(this.props.children);
- if (this.props.trigger === 'manual') {
- return child;
- }
+ const trigger = React.Children.only(this.props.children);
- const props = {};
+ const props = {
+ 'aria-describedby': this.props.overlay.props.id
+ };
- props.onClick = createChainedFunction(child.props.onClick, this.props.onClick);
- if (isOneOf('click', this.props.trigger)) {
- props.onClick = createChainedFunction(this.toggle, props.onClick);
- }
+ // create in render otherwise owner is lost...
+ this._overlay = this.getOverlay();
- if (isOneOf('hover', this.props.trigger)) {
- props.onMouseEnter = createChainedFunction(this.handleDelayedShow, this.props.onMouseEnter);
- props.onMouseLeave = createChainedFunction(this.handleDelayedHide, this.props.onMouseLeave);
- }
+ if (this.props.trigger !== 'manual') {
+
+ props.onClick = createChainedFunction(trigger.props.onClick, this.props.onClick);
- if (isOneOf('focus', this.props.trigger)) {
- props.onFocus = createChainedFunction(this.handleDelayedShow, this.props.onFocus);
- props.onBlur = createChainedFunction(this.handleDelayedHide, this.props.onBlur);
+ if (isOneOf('click', this.props.trigger)) {
+ props.onClick = createChainedFunction(this.toggle, props.onClick);
+ }
+
+ if (isOneOf('hover', this.props.trigger)) {
+ warning(!(this.props.trigger === 'hover'),
+ '[react-bootstrap] Specifying only the `"hover"` trigger limits the visibilty of the overlay to just mouse users. ' +
+ 'Consider also including the `"focus"` trigger so that touch and keyboard only users can see the overlay as well.');
+
+ props.onMouseOver = createChainedFunction(this.handleDelayedShow, this.props.onMouseOver);
+ props.onMouseOut = createChainedFunction(this.handleDelayedHide, this.props.onMouseOut);
+ }
+
+ if (isOneOf('focus', this.props.trigger)) {
+ props.onFocus = createChainedFunction(this.handleDelayedShow, this.props.onFocus);
+ props.onBlur = createChainedFunction(this.handleDelayedHide, this.props.onBlur);
+ }
+ }
+ else {
+ deprecationWarning('"manual" trigger type', ' the Overlay component');
}
return cloneElement(
- child,
+ trigger,
props
);
},
- componentWillUnmount() {
- clearTimeout(this._hoverDelay);
- },
-
- componentDidMount() {
- if (this.props.defaultOverlayShown) {
- this.updateOverlayPosition();
- }
- },
-
handleDelayedShow() {
if (this._hoverDelay != null) {
clearTimeout(this._hoverDelay);
@@ -167,10 +214,10 @@ const OverlayTrigger = React.createClass({
return;
}
- this._hoverDelay = setTimeout(function() {
+ this._hoverDelay = setTimeout(() => {
this._hoverDelay = null;
this.show();
- }.bind(this), delay);
+ }, delay);
},
handleDelayedHide() {
@@ -188,133 +235,38 @@ const OverlayTrigger = React.createClass({
return;
}
- this._hoverDelay = setTimeout(function() {
+ this._hoverDelay = setTimeout(() => {
this._hoverDelay = null;
this.hide();
- }.bind(this), delay);
- },
-
- updateOverlayPosition() {
- if (!this.isMounted()) {
- return;
- }
-
- this.setState(this.calcOverlayPosition());
+ }, delay);
},
+ // deprecated Methods
calcOverlayPosition() {
- const childOffset = this.getPosition();
-
- const overlayNode = this.getOverlayDOMNode();
- const overlayHeight = overlayNode.offsetHeight;
- const overlayWidth = overlayNode.offsetWidth;
-
- const placement = this.props.placement;
- let overlayLeft, overlayTop, arrowOffsetLeft, arrowOffsetTop;
-
- if (placement === 'left' || placement === 'right') {
- overlayTop = childOffset.top + (childOffset.height - overlayHeight) / 2;
-
- if (placement === 'left') {
- overlayLeft = childOffset.left - overlayWidth;
- } else {
- overlayLeft = childOffset.left + childOffset.width;
- }
-
- const topDelta = this._getTopDelta(overlayTop, overlayHeight);
- overlayTop += topDelta;
- arrowOffsetTop = 50 * (1 - 2 * topDelta / overlayHeight) + '%';
- arrowOffsetLeft = null;
- } else if (placement === 'top' || placement === 'bottom') {
- overlayLeft = childOffset.left + (childOffset.width - overlayWidth) / 2;
-
- if (placement === 'top') {
- overlayTop = childOffset.top - overlayHeight;
- } else {
- overlayTop = childOffset.top + childOffset.height;
- }
-
- const leftDelta = this._getLeftDelta(overlayLeft, overlayWidth);
- overlayLeft += leftDelta;
- arrowOffsetLeft = 50 * (1 - 2 * leftDelta / overlayWidth) + '%';
- arrowOffsetTop = null;
- } else {
- throw new Error(
- 'calcOverlayPosition(): No such placement of "' +
- this.props.placement + '" found.'
- );
- }
-
- return {overlayLeft, overlayTop, arrowOffsetLeft, arrowOffsetTop};
- },
+ let overlay = this.props.overlay;
- _getTopDelta(top, overlayHeight) {
- const containerDimensions = this._getContainerDimensions();
- const containerScroll = containerDimensions.scroll;
- const containerHeight = containerDimensions.height;
+ deprecationWarning('OverlayTrigger.calcOverlayPosition()', 'utils/overlayPositionUtils');
- const padding = this.props.containerPadding;
- const topEdgeOffset = top - padding - containerScroll;
- const bottomEdgeOffset = top + padding - containerScroll + overlayHeight;
-
- if (topEdgeOffset < 0) {
- return -topEdgeOffset;
- } else if (bottomEdgeOffset > containerHeight) {
- return containerHeight - bottomEdgeOffset;
- } else {
- return 0;
- }
- },
-
- _getLeftDelta(left, overlayWidth) {
- const containerDimensions = this._getContainerDimensions();
- const containerWidth = containerDimensions.width;
-
- const padding = this.props.containerPadding;
- const leftEdgeOffset = left - padding;
- const rightEdgeOffset = left + padding + overlayWidth;
-
- if (leftEdgeOffset < 0) {
- return -leftEdgeOffset;
- } else if (rightEdgeOffset > containerWidth) {
- return containerWidth - rightEdgeOffset;
- } else {
- return 0;
- }
- },
-
- _getContainerDimensions() {
- const containerNode = this.getContainerDOMNode();
- let width, height, scroll;
-
- if (containerNode.tagName === 'BODY') {
- width = window.innerWidth;
- height = window.innerHeight;
- scroll =
- domUtils.ownerDocument(containerNode).documentElement.scrollTop ||
- containerNode.scrollTop;
- } else {
- width = containerNode.offsetWidth;
- height = containerNode.offsetHeight;
- scroll = containerNode.scrollTop;
- }
-
- return {width, height, scroll};
+ return position.calcOverlayPosition(
+ overlay.props.placement || this.props.placement
+ , React.findDOMNode(overlay)
+ , React.findDOMNode(this)
+ , React.findDOMNode(overlay.props.container || this.props.container)
+ , overlay.props.containerPadding || this.props.containerPadding
+ );
},
getPosition() {
- const node = React.findDOMNode(this);
- const container = this.getContainerDOMNode();
+ deprecationWarning('OverlayTrigger.getPosition()', 'utils/overlayPositionUtils');
- const offset = container.tagName === 'BODY' ?
- domUtils.getOffset(node) : domUtils.getPosition(node, container);
+ let overlay = this.props.overlay;
- return {
- ...offset,
- height: node.offsetHeight,
- width: node.offsetWidth
- };
+ return position.getPosition(
+ React.findDOMNode(this)
+ , React.findDOMNode(overlay.props.container || this.props.container)
+ );
}
+
});
/**
diff --git a/test/OverlayMixinSpec.js b/test/OverlayMixinSpec.js
index d1a52fd282..66818d48f5 100644
--- a/test/OverlayMixinSpec.js
+++ b/test/OverlayMixinSpec.js
@@ -1,6 +1,7 @@
import React from 'react';
import ReactTestUtils from 'react/lib/ReactTestUtils';
import OverlayMixin from '../src/OverlayMixin';
+import { shouldWarn } from './helpers';
describe('OverlayMixin', function () {
let instance;
@@ -17,10 +18,15 @@ describe('OverlayMixin', function () {
}
});
+
afterEach(function() {
if (instance && ReactTestUtils.isCompositeComponent(instance) && instance.isMounted()) {
React.unmountComponentAtNode(React.findDOMNode(instance));
}
+
+ if ( console.warn.called ) {
+ shouldWarn('Overlay mixin is deprecated');
+ }
});
it('Should render overlay into container (DOMNode)', function() {
diff --git a/test/OverlayTriggerSpec.js b/test/OverlayTriggerSpec.js
index 756cb4a46d..1d871151d8 100644
--- a/test/OverlayTriggerSpec.js
+++ b/test/OverlayTriggerSpec.js
@@ -79,103 +79,6 @@ describe('OverlayTrigger', function() {
contextSpy.calledWith('value').should.be.true;
});
- describe('#calcOverlayPosition()', function() {
- [
- {
- placement: 'left',
- noOffset: [50, 300, null, '50%'],
- offsetBefore: [-200, 150, null, '0%'],
- offsetAfter: [300, 450, null, '100%']
- },
- {
- placement: 'top',
- noOffset: [200, 150, '50%', null],
- offsetBefore: [50, -100, '0%', null],
- offsetAfter: [350, 400, '100%', null]
- },
- {
- placement: 'bottom',
- noOffset: [200, 450, '50%', null],
- offsetBefore: [50, 200, '0%', null],
- offsetAfter: [350, 700, '100%', null]
- },
- {
- placement: 'right',
- noOffset: [350, 300, null, '50%'],
- offsetBefore: [100, 150, null, '0%'],
- offsetAfter: [600, 450, null, '100%']
- }
- ].forEach(function(testCase) {
- describe(`placement = ${testCase.placement}`, function() {
- let instance;
-
- beforeEach(function() {
- instance = ReactTestUtils.renderIntoDocument(
- test}
- >
- button
-
- );
-
- instance.getOverlayDOMNode = sinon.stub().returns({
- offsetHeight: 200, offsetWidth: 200
- });
- instance._getContainerDimensions = sinon.stub().returns({
- width: 600, height: 600, scroll: 100
- });
- });
-
- function checkPosition(expected) {
- const [
- overlayLeft,
- overlayTop,
- arrowOffsetLeft,
- arrowOffsetTop
- ] = expected;
-
- it('Should calculate the correct position', function() {
- instance.calcOverlayPosition().should.eql(
- {overlayLeft, overlayTop, arrowOffsetLeft, arrowOffsetTop}
- );
- });
- }
-
- describe('no viewport offset', function() {
- beforeEach(function() {
- instance.getPosition = sinon.stub().returns({
- left: 250, top: 350, width: 100, height: 100
- });
- });
-
- checkPosition(testCase.noOffset);
- });
-
- describe('viewport offset before', function() {
- beforeEach(function() {
- instance.getPosition = sinon.stub().returns({
- left: 0, top: 100, width: 100, height: 100
- });
- });
-
- checkPosition(testCase.offsetBefore);
- });
-
- describe('viewport offset after', function() {
- beforeEach(function() {
- instance.getPosition = sinon.stub().returns({
- left: 500, top: 600, width: 100, height: 100
- });
- });
-
- checkPosition(testCase.offsetAfter);
- });
- });
- });
- });
-
describe('overlay types', function() {
[
{
@@ -227,8 +130,8 @@ describe('OverlayTrigger', function() {
test}
trigger='click' rootClose={testCase.rootClose}
- >
- button
+ >
+ button
);
const overlayTrigger = React.findDOMNode(instance);