diff --git a/src-docs/src/views/tool_tip/tool_tip.js b/src-docs/src/views/tool_tip/tool_tip.js
index ea7d5ca0fc7a..d82979cf1379 100644
--- a/src-docs/src/views/tool_tip/tool_tip.js
+++ b/src-docs/src/views/tool_tip/tool_tip.js
@@ -55,8 +55,8 @@ export default () => (
-
- alert('Buttons are still clickable within tooltips.')}>Hover over me
+ Works on any kind of element — buttons, inputs, you name it!
}>
+ alert('Buttons are still clickable within tooltips.')}>Hover me
);
diff --git a/src/components/tool_tip/tool_tip.js b/src/components/tool_tip/tool_tip.js
index b56bc3da12ab..3c375e814682 100644
--- a/src/components/tool_tip/tool_tip.js
+++ b/src/components/tool_tip/tool_tip.js
@@ -38,12 +38,12 @@ export class EuiToolTip extends Component {
this.setState({ visible: true });
};
- positionToolTip = (toolTipRect) => {
- const wrapperRect = this.wrapper.getBoundingClientRect();
- const userPosition = this.props.position;
+ positionToolTip = (toolTipBounds) => {
+ const anchorBounds = this.anchor.getBoundingClientRect();
+ const requestedPosition = this.props.position;
- const calculatedPosition = calculatePopoverPosition(wrapperRect, toolTipRect, userPosition);
- const toolTipStyles = calculatePopoverStyles(wrapperRect, toolTipRect, calculatedPosition);
+ const calculatedPosition = calculatePopoverPosition(anchorBounds, toolTipBounds, requestedPosition);
+ const toolTipStyles = calculatePopoverStyles(anchorBounds, toolTipBounds, calculatedPosition);
this.setState({
visible: true,
@@ -111,7 +111,7 @@ export class EuiToolTip extends Component {
}
const trigger = (
- this.wrapper = wrapper}>
+ this.anchor = anchor}>
{cloneElement(children, {
onFocus: this.showToolTip,
onBlur: this.hideToolTip,
diff --git a/src/components/tool_tip/tool_tip_popover.js b/src/components/tool_tip/tool_tip_popover.js
index 41063bff46bd..3d5c1789c226 100644
--- a/src/components/tool_tip/tool_tip_popover.js
+++ b/src/components/tool_tip/tool_tip_popover.js
@@ -12,26 +12,20 @@ export class EuiToolTipPopover extends Component {
positionToolTip: PropTypes.func.isRequired,
}
- constructor(props) {
- super(props);
-
- this.updateDimensions = this.updateDimensions.bind(this);
- }
-
- componentDidMount() {
- document.body.classList.add('euiBody-hasToolTip');
-
- this.updateDimensions();
- window.addEventListener('resize', this.updateDimensions);
- }
-
- updateDimensions() {
+ updateDimensions = () => {
requestAnimationFrame(() => {
// Because of this delay, sometimes `positionToolTip` becomes unavailable.
if (this.popover) {
this.props.positionToolTip(this.popover.getBoundingClientRect());
}
});
+ };
+
+ componentDidMount() {
+ document.body.classList.add('euiBody-hasToolTip');
+
+ this.updateDimensions();
+ window.addEventListener('resize', this.updateDimensions);
}
componentWillUnmount() {
diff --git a/src/services/popover/popover_calculate_position.js b/src/services/popover/popover_calculate_position.js
index afa6e9f3ab5b..11af9df5eb46 100644
--- a/src/services/popover/popover_calculate_position.js
+++ b/src/services/popover/popover_calculate_position.js
@@ -1,60 +1,73 @@
-
/**
- * Determine the best position for a popup that avoids clipping by the window view port.
+ * Determine the best position for a popover that avoids clipping by the window view port.
*
- * @param {native DOM Element} wrapperRect - getBoundingClientRect() of wrapping node around the popover.
- * @param {native DOM Element} popupRect - getBoundingClientRect() of the popup node.
+ * @param {native DOM Element} anchorBounds - getBoundingClientRect() of the node the popover is tethered to (e.g. a button).
+ * @param {native DOM Element} popoverBounds - getBoundingClientRect() of the popover node (e.g. the tooltip).
* @param {string} requestedPosition - Position the user wants. One of ["top", "right", "bottom", "left"]
- * @param {number} buffer - The space between the wrapper and the popup. Also the minimum space between the popup and the window.
+ * @param {number} buffer - The space between the wrapper and the popover. Also the minimum space between the popover and the window.
*
- * @returns {string} One of ["top", "right", "bottom", "left"] that ensures no window overflow.
+ * @returns {string} One of ["top", "right", "bottom", "left"] that ensures the least amount of window overflow.
*/
-export function calculatePopoverPosition(wrapperRect, popupRect, requestedPosition, buffer = 16) {
-
- // determine popup overflow in each direction
- // negative values signal window overflow, large values signal lots of free space
- const popupOverflow = {
- top: wrapperRect.top - (popupRect.height + (2 * buffer)),
- right: window.innerWidth - wrapperRect.right - (popupRect.width + (2 * buffer)),
- left: wrapperRect.left - (popupRect.width + (2 * buffer)),
- bottom: window.innerHeight - wrapperRect.bottom - (popupRect.height + (2 * buffer)),
- };
- function hasCrossDimensionOverflow(key) {
- if (key === 'left' || key === 'right') {
- const domNodeCenterY = wrapperRect.top + (wrapperRect.height / 2);
- const tooltipTop = domNodeCenterY - ((popupRect.height / 2) + buffer);
- if (tooltipTop <= 0) {
- return true;
- }
- const tooltipBottom = domNodeCenterY + (popupRect.height / 2) + buffer;
- if (tooltipBottom >= window.innerHeight) {
- return true;
- }
- } else {
- const domNodeCenterX = wrapperRect.left + (wrapperRect.width / 2);
- const tooltipLeft = domNodeCenterX - ((popupRect.width / 2) + buffer);
- if (tooltipLeft <= 0) {
- return true;
- }
- const tooltipRight = domNodeCenterX + (popupRect.width / 2) + buffer;
- if (tooltipRight >= window.innerWidth) {
- return true;
- }
- }
- return false;
- }
+const getVisibleArea = (bounds, windowWidth, windowHeight) => {
+ const { left, top, width, height } = bounds;
+ // This is a common algorithm for finding the intersected area among two rectangles.
+ const dx = Math.min(left + width, windowWidth) - Math.max(left, 0);
+ const dy = Math.min(top + height, windowHeight) - Math.max(top, 0);
+ return dx * dy;
+};
+
+const positionAtTop = (anchorBounds, width, height, buffer) => {
+ const widthDifference = width - anchorBounds.width;
+ const left = (anchorBounds.left - widthDifference) * 0.5;
+ const top = anchorBounds.top - height - buffer;
+ return { left, top, width, height };
+};
+
+const positionAtRight = (anchorBounds, width, height, buffer) => {
+ const left = anchorBounds.right + buffer;
+ const heightDifference = (height - anchorBounds.height) * 0.5;
+ const top = anchorBounds.top - heightDifference;
+ return { left, top, width, height };
+};
+
+const positionAtBottom = (anchorBounds, width, height, buffer) => {
+ const widthDifference = width - anchorBounds.width;
+ const left = (anchorBounds.left - widthDifference) * 0.5;
+ const top = anchorBounds.bottom + buffer;
+ return { left, top, width, height };
+};
+
+const positionAtLeft = (anchorBounds, width, height, buffer) => {
+ const left = anchorBounds.left - width - buffer;
+ const heightDifference = (height - anchorBounds.height) * 0.5;
+ const top = anchorBounds.top - heightDifference;
+ return { left, top, width, height };
+};
+export function calculatePopoverPosition(anchorBounds, popoverBounds, requestedPosition, buffer = 16) {
+ const windowWidth = window.innerWidth;
+ const windowHeight = window.innerHeight;
+
+ const { width: popoverWidth, height: popoverHeight } = popoverBounds;
+
+ // Calculate how much area of the popover is visible at each position.
+ const positionToVisibleAreaMap = {
+ top: getVisibleArea(positionAtTop(anchorBounds, popoverWidth, popoverHeight, buffer), windowWidth, windowHeight),
+ right: getVisibleArea(positionAtRight(anchorBounds, popoverWidth, popoverHeight, buffer), windowWidth, windowHeight),
+ bottom: getVisibleArea(positionAtBottom(anchorBounds, popoverWidth, popoverHeight, buffer), windowWidth, windowHeight),
+ left: getVisibleArea(positionAtLeft(anchorBounds, popoverWidth, popoverHeight, buffer), windowWidth, windowHeight),
+ };
+
+ // Default to use the requested position.
let calculatedPopoverPosition = requestedPosition;
- if (popupOverflow[requestedPosition] <= 0 || hasCrossDimensionOverflow(requestedPosition)) {
- // requested position overflows window bounds
- // select direction what has the most free space
- Object.keys(popupOverflow).forEach((key) => {
- if (popupOverflow[key] > popupOverflow[calculatedPopoverPosition] && !hasCrossDimensionOverflow(key)) {
- calculatedPopoverPosition = key;
- }
- });
- }
+
+ // If the requested position clips the popover, find the position which clips the popover the least.
+ Object.keys(positionToVisibleAreaMap).forEach((position) => {
+ if (positionToVisibleAreaMap[position] > positionToVisibleAreaMap[calculatedPopoverPosition]) {
+ calculatedPopoverPosition = position;
+ }
+ });
return calculatedPopoverPosition;
}