Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update calculatePopoverPosition to seek the position with the largest amount of visible surface area of the popover. #550

Merged
merged 6 commits into from
Mar 21, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
);
20 changes: 12 additions & 8 deletions src/components/tool_tip/tool_tip.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
});
};
Expand Down Expand Up @@ -111,7 +115,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
1 change: 0 additions & 1 deletion src/services/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,4 @@ export {

export {
calculatePopoverPosition,
calculatePopoverStyles,
} from './popover';
1 change: 0 additions & 1 deletion src/services/popover/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { calculatePopoverPosition } from './popover_calculate_position';
export { calculatePopoverStyles } from './popover_calculate_styles';
123 changes: 73 additions & 50 deletions src/services/popover/popover_calculate_position.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,83 @@

/**
* 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this comment get detached from the function it describes.

*
* @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)),

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

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

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

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

return {
position: calculatedPopoverPosition,
...positionToBoundsMap[calculatedPopoverPosition],
};
}
19 changes: 0 additions & 19 deletions src/services/popover/popover_calculate_styles.js

This file was deleted.