Merge pull request #3228 from matrix-org/jryans/tooltip-safe-area-alt

Improve interactive tooltip safe mousing area
pull/21833/head
J. Ryan Stinnett 2019-07-17 14:28:57 +01:00 committed by GitHub
commit 35ba489a2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 138 additions and 10 deletions

View File

@ -36,12 +36,13 @@ limitations under the License.
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
// tooltip overhang + action bar + action bar offset from event // tooltip safe mousing area + tooltip overhang +
width: calc(48px + 100% + 8px); // action bar + action bar offset from event
width: calc(10px + 48px + 100% + 8px);
// safe area + action bar // safe area + action bar
height: calc(20px + 100%); height: calc(20px + 100%);
top: -20px; top: -20px;
left: -48px; left: -58px;
z-index: -1; z-index: -1;
cursor: initial; cursor: initial;
} }

View File

@ -37,12 +37,55 @@ function getOrCreateContainer() {
return container; return container;
} }
function isInRect(x, y, rect, buffer = 10) { function isInRect(x, y, rect, { buffer = 0 } = {}) {
const { top, right, bottom, left } = rect; const { top, right, bottom, left } = rect;
return x >= (left - buffer) && x <= (right + buffer) return x >= (left - buffer) && x <= (right + buffer)
&& y >= (top - buffer) && y <= (bottom + buffer); && y >= (top - buffer) && y <= (bottom + buffer);
} }
/**
* Returns the positive slope of the diagonal of the rect.
*
* @param {DOMRect} rect
* @return {integer}
*/
function getDiagonalSlope(rect) {
const { top, right, bottom, left } = rect;
return (bottom - top) / (right - left);
}
function isInUpperLeftHalf(x, y, rect) {
const { bottom, left } = rect;
// Negative slope because Y values grow downwards and for this case, the
// diagonal goes from larger to smaller Y values.
const diagonalSlope = getDiagonalSlope(rect) * -1;
return isInRect(x, y, rect) && (y <= bottom + diagonalSlope * (x - left));
}
function isInLowerRightHalf(x, y, rect) {
const { bottom, left } = rect;
// Negative slope because Y values grow downwards and for this case, the
// diagonal goes from larger to smaller Y values.
const diagonalSlope = getDiagonalSlope(rect) * -1;
return isInRect(x, y, rect) && (y >= bottom + diagonalSlope * (x - left));
}
function isInUpperRightHalf(x, y, rect) {
const { top, left } = rect;
// Positive slope because Y values grow downwards and for this case, the
// diagonal goes from smaller to larger Y values.
const diagonalSlope = getDiagonalSlope(rect) * 1;
return isInRect(x, y, rect) && (y <= top + diagonalSlope * (x - left));
}
function isInLowerLeftHalf(x, y, rect) {
const { top, left } = rect;
// Positive slope because Y values grow downwards and for this case, the
// diagonal goes from smaller to larger Y values.
const diagonalSlope = getDiagonalSlope(rect) * 1;
return isInRect(x, y, rect) && (y >= top + diagonalSlope * (x - left));
}
/* /*
* This style of tooltip takes a "target" element as its child and centers the * This style of tooltip takes a "target" element as its child and centers the
* tooltip along one edge of the target. * tooltip along one edge of the target.
@ -91,15 +134,99 @@ export default class InteractiveTooltip extends React.Component {
this.target = element; this.target = element;
} }
canTooltipFitAboveTarget() {
const { contentRect } = this.state;
const targetRect = this.target.getBoundingClientRect();
const targetTop = targetRect.top + window.pageYOffset;
return (
!contentRect ||
(targetTop - contentRect.height > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE)
);
}
onMouseMove = (ev) => { onMouseMove = (ev) => {
const { clientX: x, clientY: y } = ev; const { clientX: x, clientY: y } = ev;
const { contentRect } = this.state; const { contentRect } = this.state;
const targetRect = this.target.getBoundingClientRect(); const targetRect = this.target.getBoundingClientRect();
if (!isInRect(x, y, contentRect) && !isInRect(x, y, targetRect)) { // When moving the mouse from the target to the tooltip, we create a
this.hideTooltip(); // safe area that includes the tooltip, the target, and the trapezoid
// ABCD between them:
// ┌───────────┐
// │ │
// │ │
// A └───E───F───┘ B
// V
// ┌─┐
// │ │
// C└─┘D
//
// As long as the mouse remains inside the safe area, the tooltip will
// stay open.
const buffer = 10;
if (
isInRect(x, y, contentRect, { buffer }) ||
isInRect(x, y, targetRect)
) {
return; return;
} }
if (this.canTooltipFitAboveTarget()) {
const trapezoidLeft = {
top: contentRect.bottom,
right: targetRect.left,
bottom: targetRect.bottom,
left: contentRect.left - buffer,
};
const trapezoidCenter = {
top: contentRect.bottom,
right: targetRect.right,
bottom: targetRect.bottom,
left: targetRect.left,
};
const trapezoidRight = {
top: contentRect.bottom,
right: contentRect.right + buffer,
bottom: targetRect.bottom,
left: targetRect.right,
};
if (
isInUpperRightHalf(x, y, trapezoidLeft) ||
isInRect(x, y, trapezoidCenter) ||
isInUpperLeftHalf(x, y, trapezoidRight)
) {
return;
}
} else {
const trapezoidLeft = {
top: targetRect.top,
right: targetRect.left,
bottom: contentRect.top,
left: contentRect.left - buffer,
};
const trapezoidCenter = {
top: targetRect.top,
right: targetRect.right,
bottom: contentRect.top,
left: targetRect.left,
};
const trapezoidRight = {
top: targetRect.top,
right: contentRect.right + buffer,
bottom: contentRect.top,
left: targetRect.right,
};
if (
isInLowerRightHalf(x, y, trapezoidLeft) ||
isInRect(x, y, trapezoidCenter) ||
isInLowerLeftHalf(x, y, trapezoidRight)
) {
return;
}
}
this.hideTooltip();
} }
onTargetMouseOver = (ev) => { onTargetMouseOver = (ev) => {
@ -149,12 +276,12 @@ export default class InteractiveTooltip extends React.Component {
// edge, flip around to below the target. // edge, flip around to below the target.
const position = {}; const position = {};
let chevronFace = null; let chevronFace = null;
if (contentRect && (targetTop - contentRect.height <= MIN_SAFE_DISTANCE_TO_WINDOW_EDGE)) { if (this.canTooltipFitAboveTarget()) {
position.top = targetBottom;
chevronFace = "top";
} else {
position.bottom = window.innerHeight - targetTop; position.bottom = window.innerHeight - targetTop;
chevronFace = "bottom"; chevronFace = "bottom";
} else {
position.top = targetBottom;
chevronFace = "top";
} }
// Center the tooltip horizontally with the target's center. // Center the tooltip horizontally with the target's center.