From 263dd95989fb3b8a88aa7e91e92c3af09bb9d27a Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 20 Mar 2018 17:49:33 -0700 Subject: [PATCH 1/6] Update calculatePopoverPosition to seek the position with the largest amount of visible surface area of the popover. --- src-docs/src/views/tool_tip/tool_tip.js | 4 +- src/components/tool_tip/tool_tip.js | 12 +- src/components/tool_tip/tool_tip_popover.js | 22 ++-- .../popover/popover_calculate_position.js | 110 ++++++++++-------- 4 files changed, 77 insertions(+), 71 deletions(-) diff --git a/src-docs/src/views/tool_tip/tool_tip.js b/src-docs/src/views/tool_tip/tool_tip.js index ea7d5ca0fc7..d82979cf137 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 b56bc3da12a..3c375e81468 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 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/popover/popover_calculate_position.js b/src/services/popover/popover_calculate_position.js index afa6e9f3ab5..c3115783e60 100644 --- a/src/services/popover/popover_calculate_position.js +++ b/src/services/popover/popover_calculate_position.js @@ -1,60 +1,72 @@ - /** - * 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; } From c376e4b6f2b1633e977560f54b8143866baa4633 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 20 Mar 2018 18:33:55 -0700 Subject: [PATCH 2/6] Return position data from calculatePopoverPosition. - Fix mistakes with calculating height and width differences. - Remove calculatePopoverStyles. --- src/components/tool_tip/tool_tip.js | 12 ++++-- src/services/index.js | 1 - src/services/popover/index.js | 1 - .../popover/popover_calculate_position.js | 39 ++++++++++++------- .../popover/popover_calculate_styles.js | 19 --------- 5 files changed, 33 insertions(+), 39 deletions(-) delete mode 100644 src/services/popover/popover_calculate_styles.js diff --git a/src/components/tool_tip/tool_tip.js b/src/components/tool_tip/tool_tip.js index 3c375e81468..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'; @@ -42,12 +42,16 @@ export class EuiToolTip extends Component { const anchorBounds = this.anchor.getBoundingClientRect(); const requestedPosition = this.props.position; - const calculatedPosition = calculatePopoverPosition(anchorBounds, toolTipBounds, requestedPosition); - const toolTipStyles = calculatePopoverStyles(anchorBounds, toolTipBounds, calculatedPosition); + const { position, left, top } = calculatePopoverPosition(anchorBounds, toolTipBounds, requestedPosition); + + const toolTipStyles = { + top: top + window.scrollY, + left, + }; this.setState({ visible: true, - calculatedPosition, + calculatedPosition: position, toolTipStyles, }); }; 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/index.js b/src/services/popover/index.js index 28cdd4279cc..28fd6132873 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'; diff --git a/src/services/popover/popover_calculate_position.js b/src/services/popover/popover_calculate_position.js index c3115783e60..aaac139cf5d 100644 --- a/src/services/popover/popover_calculate_position.js +++ b/src/services/popover/popover_calculate_position.js @@ -19,29 +19,29 @@ const getVisibleArea = (bounds, windowWidth, windowHeight) => { const positionAtTop = (anchorBounds, width, height, buffer) => { const widthDifference = width - anchorBounds.width; - const left = (anchorBounds.left - widthDifference) * 0.5; + 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; + 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 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; + const heightDifference = height - anchorBounds.height; + const top = anchorBounds.top - heightDifference * 0.5; return { left, top, width, height }; }; @@ -50,23 +50,34 @@ export function calculatePopoverPosition(anchorBounds, popoverBounds, requestedP 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), + 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); + }); + // Default to use the requested position. let calculatedPopoverPosition = requestedPosition; // If the requested position clips the popover, find the position which clips the popover the least. - Object.keys(positionToVisibleAreaMap).forEach((position) => { + positions.forEach((position) => { if (positionToVisibleAreaMap[position] > positionToVisibleAreaMap[calculatedPopoverPosition]) { calculatedPopoverPosition = position; } }); - return calculatedPopoverPosition; + return { + position: calculatedPopoverPosition, + ...positionToBoundsMap[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; -} From 36a890c86a286d653d3dc5f9c098f62315b4eddb Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 20 Mar 2018 20:19:42 -0700 Subject: [PATCH 3/6] Put comment back where it belongs. Fix EuiIconTip example. --- src-docs/src/views/tool_tip/icon_tip.js | 2 +- .../popover/popover_calculate_position.js | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) 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 () => ( - + { const { left, top, width, height } = bounds; // This is a common algorithm for finding the intersected area among two rectangles. @@ -45,6 +34,16 @@ const positionAtLeft = (anchorBounds, width, height, buffer) => { return { left, top, width, height }; }; +/** + * Determine the best position for a popover that avoids clipping by the window view port. + * + * @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 popover. Also the minimum space between the popover and the window. + * + * @returns {string} One of ["top", "right", "bottom", "left"] that ensures the least amount of window overflow. + */ export function calculatePopoverPosition(anchorBounds, popoverBounds, requestedPosition, buffer = 16) { const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; From 2de8f3ee152e56f718acd432e2c50a2508f2d361 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 20 Mar 2018 20:21:08 -0700 Subject: [PATCH 4/6] Rename popver_calculate_position to calculate_popover_position. --- ...over_calculate_position.js => calculate_popover_position.js} | 0 src/services/popover/index.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/services/popover/{popover_calculate_position.js => calculate_popover_position.js} (100%) diff --git a/src/services/popover/popover_calculate_position.js b/src/services/popover/calculate_popover_position.js similarity index 100% rename from src/services/popover/popover_calculate_position.js rename to src/services/popover/calculate_popover_position.js diff --git a/src/services/popover/index.js b/src/services/popover/index.js index 28fd6132873..5fc1aeb6ec1 100644 --- a/src/services/popover/index.js +++ b/src/services/popover/index.js @@ -1 +1 @@ -export { calculatePopoverPosition } from './popover_calculate_position'; +export { calculatePopoverPosition } from './calculate_popover_position'; From 18f56578c0e1b319bf2f70e5179bee0695277229 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 20 Mar 2018 20:22:06 -0700 Subject: [PATCH 5/6] Update CHANGELOG. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) 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)) From b72356fbf02392c560a0ecdeef14d2cae92c65d1 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 20 Mar 2018 20:33:23 -0700 Subject: [PATCH 6/6] Clarify calculatePopoverPosition comments and use array reduce() method. --- .../popover/calculate_popover_position.js | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/services/popover/calculate_popover_position.js b/src/services/popover/calculate_popover_position.js index bb60d0dcd39..00b824df525 100644 --- a/src/services/popover/calculate_popover_position.js +++ b/src/services/popover/calculate_popover_position.js @@ -37,12 +37,12 @@ const positionAtLeft = (anchorBounds, width, height, buffer) => { /** * Determine the best position for a popover that avoids clipping by the window view port. * - * @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 {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 {string} One of ["top", "right", "bottom", "left"] that ensures the least amount of window overflow. + * @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; @@ -60,20 +60,18 @@ export function calculatePopoverPosition(anchorBounds, popoverBounds, requestedP // Calculate how much area of the popover is visible at each position. const positionToVisibleAreaMap = {}; - positions.forEach((position) => { positionToVisibleAreaMap[position] = getVisibleArea(positionToBoundsMap[position], windowWidth, windowHeight); }); - // Default to use the requested position. - let calculatedPopoverPosition = requestedPosition; - // If the requested position clips the popover, find the position which clips the popover the least. - positions.forEach((position) => { - if (positionToVisibleAreaMap[position] > positionToVisibleAreaMap[calculatedPopoverPosition]) { - calculatedPopoverPosition = position; + // 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,