Skip to content

Commit

Permalink
Update calculatePopoverPosition to seek the position with the largest…
Browse files Browse the repository at this point in the history
… amount of visible surface area of the popover.
  • Loading branch information
cjcenizal committed Mar 21, 2018
1 parent a555d65 commit 90a6de5
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 71 deletions.
4 changes: 2 additions & 2 deletions src-docs/src/views/tool_tip/tool_tip.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export default () => (

<EuiSpacer />

<EuiToolTip position="top" content="Works on anything">
<EuiButton onClick={() => alert('Buttons are still clickable within tooltips.')}>Hover over me</EuiButton>
<EuiToolTip position="top" content={<p>Works on any kind of element &mdash; buttons, inputs, you name it!</p>}>
<EuiButton onClick={() => alert('Buttons are still clickable within tooltips.')}>Hover me</EuiButton>
</EuiToolTip>
</div>
);
12 changes: 6 additions & 6 deletions src/components/tool_tip/tool_tip.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -111,7 +111,7 @@ export class EuiToolTip extends Component {
}

const trigger = (
<span ref={wrapper => this.wrapper = wrapper}>
<span ref={anchor => this.anchor = anchor}>
{cloneElement(children, {
onFocus: this.showToolTip,
onBlur: this.hideToolTip,
Expand Down
22 changes: 8 additions & 14 deletions src/components/tool_tip/tool_tip_popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
111 changes: 62 additions & 49 deletions src/services/popover/popover_calculate_position.js
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;
}

0 comments on commit 90a6de5

Please sign in to comment.