diff --git a/CHANGELOG.md b/CHANGELOG.md
index 658b1a768af..f3e1ee46211 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
- Added `stop` and `stopFilled` icons ([#543](https://github.com/elastic/eui/pull/543))
+**Bug fixes**
+
+- Fix `EuiToolTip` smart positioning to prevent tooltip from being clipped by the window where possible ([#550]https://github.com/elastic/eui/pull/550)
+
# [`0.0.31`](https://github.com/elastic/eui/tree/v0.0.31)
- Made `` TypeScript types more specific ([#518](https://github.com/elastic/eui/pull/518))
diff --git a/src-docs/src/views/tool_tip/icon_tip.js b/src-docs/src/views/tool_tip/icon_tip.js
index e94c55bde76..1c40b4fbd10 100644
--- a/src-docs/src/views/tool_tip/icon_tip.js
+++ b/src-docs/src/views/tool_tip/icon_tip.js
@@ -10,7 +10,7 @@ import {
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 b56bc3da12a..a9883c3f94f 100644
--- a/src/components/tool_tip/tool_tip.js
+++ b/src/components/tool_tip/tool_tip.js
@@ -8,7 +8,7 @@ import classNames from 'classnames';
import { EuiPortal } from '../portal';
import { EuiToolTipPopover } from './tool_tip_popover';
-import { calculatePopoverPosition, calculatePopoverStyles } from '../../services';
+import { calculatePopoverPosition } from '../../services';
import makeId from '../form/form_row/make_id';
@@ -38,16 +38,20 @@ 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 { position, left, top } = calculatePopoverPosition(anchorBounds, toolTipBounds, requestedPosition);
+
+ const toolTipStyles = {
+ top: top + window.scrollY,
+ left,
+ };
this.setState({
visible: true,
- calculatedPosition,
+ calculatedPosition: position,
toolTipStyles,
});
};
@@ -111,7 +115,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 41063bff46b..3d5c1789c22 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/index.js b/src/services/index.js
index b4598f5f9e1..e4f06d64ea8 100644
--- a/src/services/index.js
+++ b/src/services/index.js
@@ -64,5 +64,4 @@ export {
export {
calculatePopoverPosition,
- calculatePopoverStyles,
} from './popover';
diff --git a/src/services/popover/calculate_popover_position.js b/src/services/popover/calculate_popover_position.js
new file mode 100644
index 00000000000..00b824df525
--- /dev/null
+++ b/src/services/popover/calculate_popover_position.js
@@ -0,0 +1,80 @@
+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;
+ const top = anchorBounds.top - heightDifference * 0.5;
+ 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;
+ const top = anchorBounds.top - heightDifference * 0.5;
+ return { left, top, width, height };
+};
+
+/**
+ * Determine the best position for a popover that avoids clipping by the window view port.
+ *
+ * @param {Object} anchorBounds - getBoundingClientRect() of the node the popover is tethered to (e.g. a button).
+ * @param {Object} 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 popover. Also the minimum space between the popover and the window.
+ *
+ * @returns {Object} With properties position (one of ["top", "right", "bottom", "left"]), left, top, width, and height.
+ */
+export function calculatePopoverPosition(anchorBounds, popoverBounds, requestedPosition, buffer = 16) {
+ const windowWidth = window.innerWidth;
+ const windowHeight = window.innerHeight;
+ const { width: popoverWidth, height: popoverHeight } = popoverBounds;
+
+ const positionToBoundsMap = {
+ top: positionAtTop(anchorBounds, popoverWidth, popoverHeight, buffer),
+ right: positionAtRight(anchorBounds, popoverWidth, popoverHeight, buffer),
+ bottom: positionAtBottom(anchorBounds, popoverWidth, popoverHeight, buffer),
+ left: positionAtLeft(anchorBounds, popoverWidth, popoverHeight, buffer),
+ };
+
+ const positions = Object.keys(positionToBoundsMap);
+
+ // Calculate how much area of the popover is visible at each position.
+ const positionToVisibleAreaMap = {};
+ positions.forEach((position) => {
+ positionToVisibleAreaMap[position] = getVisibleArea(positionToBoundsMap[position], windowWidth, windowHeight);
+ });
+
+ // If the requested position clips the popover, find the position which clips the popover the least.
+ // Default to use the requested position.
+ let calculatedPopoverPosition = positions.reduce((mostVisiblePosition, position) => {
+ if (positionToVisibleAreaMap[position] > positionToVisibleAreaMap[mostVisiblePosition]) {
+ return position;
+ }
+ return mostVisiblePosition;
+ }, requestedPosition);
+
+ return {
+ position: calculatedPopoverPosition,
+ ...positionToBoundsMap[calculatedPopoverPosition],
+ };
+}
diff --git a/src/services/popover/index.js b/src/services/popover/index.js
index 28cdd4279cc..5fc1aeb6ec1 100644
--- a/src/services/popover/index.js
+++ b/src/services/popover/index.js
@@ -1,2 +1 @@
-export { calculatePopoverPosition } from './popover_calculate_position';
-export { calculatePopoverStyles } from './popover_calculate_styles';
+export { calculatePopoverPosition } from './calculate_popover_position';
diff --git a/src/services/popover/popover_calculate_position.js b/src/services/popover/popover_calculate_position.js
deleted file mode 100644
index afa6e9f3ab5..00000000000
--- a/src/services/popover/popover_calculate_position.js
+++ /dev/null
@@ -1,60 +0,0 @@
-
-/**
- * Determine the best position for a popup 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 {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.
- *
- * @returns {string} One of ["top", "right", "bottom", "left"] that ensures no 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;
- }
-
- 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;
- }
- });
- }
-
- return calculatedPopoverPosition;
-}
diff --git a/src/services/popover/popover_calculate_styles.js b/src/services/popover/popover_calculate_styles.js
deleted file mode 100644
index 741d3e2c422..00000000000
--- a/src/services/popover/popover_calculate_styles.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export function calculatePopoverStyles(wrapperNodeRect, popupNodeRect, position, buffer = 16) {
- const styles = {};
-
- if (position === 'top') {
- styles.top = wrapperNodeRect.top + window.scrollY - (popupNodeRect.height + buffer);
- styles.left = wrapperNodeRect.left + (wrapperNodeRect.width / 2) - (popupNodeRect.width / 2);
- } else if (position === 'bottom') {
- styles.top = wrapperNodeRect.top + window.scrollY + wrapperNodeRect.height + buffer;
- styles.left = wrapperNodeRect.left + (wrapperNodeRect.width / 2) - (popupNodeRect.width / 2);
- } else if (position === 'right') {
- styles.top = wrapperNodeRect.top + window.scrollY - ((popupNodeRect.height - wrapperNodeRect.height) / 2);
- styles.left = wrapperNodeRect.left + wrapperNodeRect.width + buffer;
- } else if (position === 'left') {
- styles.top = wrapperNodeRect.top + window.scrollY - ((popupNodeRect.height - wrapperNodeRect.height) / 2);
- styles.left = wrapperNodeRect.left - popupNodeRect.width - buffer;
- }
-
- return styles;
-}