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; }