-
Notifications
You must be signed in to change notification settings - Fork 843
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update calculatePopoverPosition to seek the position with the largest…
… amount of visible surface area of the popover.
- Loading branch information
Showing
4 changed files
with
78 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |