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. (#550)

* Update calculatePopoverPosition to seek the position with the largest amount of visible surface area of the popover.
* Return position data from calculatePopoverPosition.
* Rename popver_calculate_position to calculate_popover_position.
  • Loading branch information
cjcenizal authored Mar 21, 2018
1 parent a555d65 commit 7351dd1
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 107 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<EuiProgress>` TypeScript types more specific ([#518](https://github.com/elastic/eui/pull/518))
Expand Down
2 changes: 1 addition & 1 deletion src-docs/src/views/tool_tip/icon_tip.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {

export default () => (
<Fragment>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiCheckbox
id="explainedCheckbox"
Expand Down
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';
80 changes: 80 additions & 0 deletions src/services/popover/calculate_popover_position.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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 };
};

/**
* Determine the best position for a popover that avoids clipping by the window view port.
*
* @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 {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;
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),
};

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

// If the requested position clips the popover, find the position which clips the popover the least.
// 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,
...positionToBoundsMap[calculatedPopoverPosition],
};
}
3 changes: 1 addition & 2 deletions 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';
export { calculatePopoverPosition } from './calculate_popover_position';
60 changes: 0 additions & 60 deletions src/services/popover/popover_calculate_position.js

This file was deleted.

19 changes: 0 additions & 19 deletions src/services/popover/popover_calculate_styles.js

This file was deleted.

0 comments on commit 7351dd1

Please sign in to comment.