mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
VizTooltip: Use previous positioning calculation (#36861)
* VizTooltip: Use previous positioning calculation * VizTooltip: Don't need to check for tooltip width/height here * VizTooltip: Disable pointer-events in the mixin * VizTooltipContainer: Move pointerEvents to inline style to prevent breaking AnnotationMarker * Move comment to correct place * No need to change this
This commit is contained in:
parent
ff56ea6ea6
commit
4f315bf48f
@ -1,7 +1,10 @@
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Portal } from '../Portal/Portal';
|
||||
import { Dimensions, TimeZone } from '@grafana/data';
|
||||
import { FlotPosition } from '../Graph/types';
|
||||
import { VizTooltipContainer } from './VizTooltipContainer';
|
||||
import { useStyles } from '../../themes';
|
||||
import { TooltipDisplayMode } from './models.gen';
|
||||
|
||||
// Describes active dimensions user interacts with
|
||||
@ -46,14 +49,30 @@ export interface VizTooltipProps {
|
||||
* @public
|
||||
*/
|
||||
export const VizTooltip: React.FC<VizTooltipProps> = ({ content, position, offset }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
if (position) {
|
||||
return (
|
||||
<VizTooltipContainer position={position} offset={offset || { x: 0, y: 0 }}>
|
||||
{content}
|
||||
</VizTooltipContainer>
|
||||
<Portal className={styles.portal}>
|
||||
<VizTooltipContainer position={position} offset={offset || { x: 0, y: 0 }}>
|
||||
{content}
|
||||
</VizTooltipContainer>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
VizTooltip.displayName = 'VizTooltip';
|
||||
|
||||
const getStyles = () => {
|
||||
return {
|
||||
portal: css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { useState, HTMLAttributes, useMemo } from 'react';
|
||||
import React, { useState, HTMLAttributes, useMemo, useRef, useLayoutEffect } from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { getTooltipContainerStyles } from '../../themes/mixins';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { usePopper } from 'react-popper';
|
||||
import useWindowSize from 'react-use/lib/useWindowSize';
|
||||
import { Dimensions2D, GrafanaTheme2 } from '@grafana/data';
|
||||
import { calculateTooltipPosition } from './utils';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -24,47 +25,76 @@ export const VizTooltipContainer: React.FC<VizTooltipContainerProps> = ({
|
||||
className,
|
||||
...otherProps
|
||||
}) => {
|
||||
const [tooltipRef, setTooltipRef] = useState<HTMLDivElement | null>(null);
|
||||
const virtualElement = useMemo(
|
||||
() => ({
|
||||
getBoundingClientRect() {
|
||||
return { top: positionY, left: positionX, bottom: positionY, right: positionX, width: 0, height: 0 };
|
||||
},
|
||||
}),
|
||||
[positionY, positionX]
|
||||
);
|
||||
const { styles: popperStyles, attributes } = usePopper(virtualElement, tooltipRef, {
|
||||
placement: 'bottom-start',
|
||||
modifiers: [
|
||||
{ name: 'arrow', enabled: false },
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
enabled: true,
|
||||
options: {
|
||||
altAxis: true,
|
||||
rootBoundary: 'viewport',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [offsetX, offsetY],
|
||||
},
|
||||
},
|
||||
],
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const [tooltipMeasurement, setTooltipMeasurement] = useState<Dimensions2D>({ width: 0, height: 0 });
|
||||
const { width, height } = useWindowSize();
|
||||
const [placement, setPlacement] = useState({
|
||||
x: positionX + offsetX,
|
||||
y: positionY + offsetY,
|
||||
});
|
||||
|
||||
const resizeObserver = useMemo(
|
||||
() =>
|
||||
// TS has hard time playing games with @types/resize-observer-browser, hence the ignore
|
||||
// @ts-ignore
|
||||
new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
const tW = Math.floor(entry.contentRect.width + 2 * 8); // adding padding until Safari supports borderBoxSize
|
||||
const tH = Math.floor(entry.contentRect.height + 2 * 8);
|
||||
if (tooltipMeasurement.width !== tW || tooltipMeasurement.height !== tH) {
|
||||
setTooltipMeasurement({
|
||||
width: tW,
|
||||
height: tH,
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
[tooltipMeasurement]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (tooltipRef.current) {
|
||||
resizeObserver.observe(tooltipRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [resizeObserver]);
|
||||
|
||||
// Make sure tooltip does not overflow window
|
||||
useLayoutEffect(() => {
|
||||
if (tooltipRef && tooltipRef.current) {
|
||||
const { x, y } = calculateTooltipPosition(
|
||||
positionX,
|
||||
positionY,
|
||||
tooltipMeasurement.width,
|
||||
tooltipMeasurement.height,
|
||||
offsetX,
|
||||
offsetY,
|
||||
width,
|
||||
height
|
||||
);
|
||||
|
||||
setPlacement({ x, y });
|
||||
}
|
||||
}, [width, height, positionX, offsetX, positionY, offsetY, tooltipMeasurement]);
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setTooltipRef}
|
||||
ref={tooltipRef}
|
||||
style={{
|
||||
...popperStyles.popper,
|
||||
display: popperStyles.popper?.transform ? 'block' : 'none',
|
||||
transition: 'all ease-out 0.2s',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
// disabling pointer-events is to prevent the tooltip from flickering when moving left to right
|
||||
// see e.g. https://github.com/grafana/grafana/pull/33609
|
||||
pointerEvents: 'none',
|
||||
top: 0,
|
||||
transform: `translate(${placement.x}px, ${placement.y}px)`,
|
||||
transition: 'transform ease-out 0.1s',
|
||||
}}
|
||||
{...attributes.popper}
|
||||
{...otherProps}
|
||||
className={cx(styles.wrapper, className)}
|
||||
>
|
||||
|
165
packages/grafana-ui/src/components/VizTooltip/utils.test.ts
Normal file
165
packages/grafana-ui/src/components/VizTooltip/utils.test.ts
Normal file
@ -0,0 +1,165 @@
|
||||
import { calculateTooltipPosition } from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('calculateTooltipPosition', () => {
|
||||
// let's pick some easy numbers for these, we shouldn't need to change them
|
||||
const tooltipWidth = 100;
|
||||
const tooltipHeight = 100;
|
||||
const xOffset = 10;
|
||||
const yOffset = 10;
|
||||
const windowWidth = 200;
|
||||
const windowHeight = 200;
|
||||
|
||||
it('sticky positions the tooltip to the right if it would overflow at both ends but overflow to the left more', () => {
|
||||
const xPos = 99;
|
||||
const yPos = 50;
|
||||
const result = calculateTooltipPosition(
|
||||
xPos,
|
||||
yPos,
|
||||
tooltipWidth,
|
||||
tooltipHeight,
|
||||
xOffset,
|
||||
yOffset,
|
||||
windowWidth,
|
||||
windowHeight
|
||||
);
|
||||
expect(result).toEqual({
|
||||
x: 90,
|
||||
y: 60,
|
||||
});
|
||||
});
|
||||
|
||||
it('sticky positions the tooltip to the left if it would overflow at both ends but overflow to the right more', () => {
|
||||
const xPos = 101;
|
||||
const yPos = 50;
|
||||
const result = calculateTooltipPosition(
|
||||
xPos,
|
||||
yPos,
|
||||
tooltipWidth,
|
||||
tooltipHeight,
|
||||
xOffset,
|
||||
yOffset,
|
||||
windowWidth,
|
||||
windowHeight
|
||||
);
|
||||
expect(result).toEqual({
|
||||
x: 10,
|
||||
y: 60,
|
||||
});
|
||||
});
|
||||
|
||||
it('positions the tooltip to left of the cursor if it would overflow right', () => {
|
||||
const xPos = 150;
|
||||
const yPos = 50;
|
||||
const result = calculateTooltipPosition(
|
||||
xPos,
|
||||
yPos,
|
||||
tooltipWidth,
|
||||
tooltipHeight,
|
||||
xOffset,
|
||||
yOffset,
|
||||
windowWidth,
|
||||
windowHeight
|
||||
);
|
||||
expect(result).toEqual({
|
||||
x: 40,
|
||||
y: 60,
|
||||
});
|
||||
});
|
||||
|
||||
it('positions the tooltip to the right of the cursor if it would not overflow', () => {
|
||||
const xPos = 50;
|
||||
const yPos = 50;
|
||||
const result = calculateTooltipPosition(
|
||||
xPos,
|
||||
yPos,
|
||||
tooltipWidth,
|
||||
tooltipHeight,
|
||||
xOffset,
|
||||
yOffset,
|
||||
windowWidth,
|
||||
windowHeight
|
||||
);
|
||||
expect(result).toEqual({
|
||||
x: 60,
|
||||
y: 60,
|
||||
});
|
||||
});
|
||||
|
||||
it('sticky positions the tooltip to the bottom if it would overflow at both ends but overflow to the top more', () => {
|
||||
const xPos = 50;
|
||||
const yPos = 99;
|
||||
const result = calculateTooltipPosition(
|
||||
xPos,
|
||||
yPos,
|
||||
tooltipWidth,
|
||||
tooltipHeight,
|
||||
xOffset,
|
||||
yOffset,
|
||||
windowWidth,
|
||||
windowHeight
|
||||
);
|
||||
expect(result).toEqual({
|
||||
x: 60,
|
||||
y: 90,
|
||||
});
|
||||
});
|
||||
|
||||
it('sticky positions the tooltip to the top if it would overflow at both ends but overflow to the bottom more', () => {
|
||||
const xPos = 50;
|
||||
const yPos = 101;
|
||||
const result = calculateTooltipPosition(
|
||||
xPos,
|
||||
yPos,
|
||||
tooltipWidth,
|
||||
tooltipHeight,
|
||||
xOffset,
|
||||
yOffset,
|
||||
windowWidth,
|
||||
windowHeight
|
||||
);
|
||||
expect(result).toEqual({
|
||||
x: 60,
|
||||
y: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('positions the tooltip above the cursor if it would overflow at the bottom', () => {
|
||||
const xPos = 50;
|
||||
const yPos = 150;
|
||||
const result = calculateTooltipPosition(
|
||||
xPos,
|
||||
yPos,
|
||||
tooltipWidth,
|
||||
tooltipHeight,
|
||||
xOffset,
|
||||
yOffset,
|
||||
windowWidth,
|
||||
windowHeight
|
||||
);
|
||||
expect(result).toEqual({
|
||||
x: 60,
|
||||
y: 40,
|
||||
});
|
||||
});
|
||||
|
||||
it('positions the tooltip below the cursor if it would not overflow', () => {
|
||||
const xPos = 50;
|
||||
const yPos = 50;
|
||||
const result = calculateTooltipPosition(
|
||||
xPos,
|
||||
yPos,
|
||||
tooltipWidth,
|
||||
tooltipHeight,
|
||||
xOffset,
|
||||
yOffset,
|
||||
windowWidth,
|
||||
windowHeight
|
||||
);
|
||||
expect(result).toEqual({
|
||||
x: 60,
|
||||
y: 60,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
40
packages/grafana-ui/src/components/VizTooltip/utils.ts
Normal file
40
packages/grafana-ui/src/components/VizTooltip/utils.ts
Normal file
@ -0,0 +1,40 @@
|
||||
export const calculateTooltipPosition = (
|
||||
xPos = 0,
|
||||
yPos = 0,
|
||||
tooltipWidth = 0,
|
||||
tooltipHeight = 0,
|
||||
xOffset = 0,
|
||||
yOffset = 0,
|
||||
windowWidth = 0,
|
||||
windowHeight = 0
|
||||
) => {
|
||||
let x = xPos;
|
||||
let y = yPos;
|
||||
|
||||
const overflowRight = Math.max(xPos + xOffset + tooltipWidth - (windowWidth - xOffset), 0);
|
||||
const overflowLeft = Math.abs(Math.min(xPos - xOffset - tooltipWidth - xOffset, 0));
|
||||
const wouldOverflowRight = overflowRight > 0;
|
||||
const wouldOverflowLeft = overflowLeft > 0;
|
||||
|
||||
const overflowBelow = Math.max(yPos + yOffset + tooltipHeight - (windowHeight - yOffset), 0);
|
||||
const overflowAbove = Math.abs(Math.min(yPos - yOffset - tooltipHeight - yOffset, 0));
|
||||
const wouldOverflowBelow = overflowBelow > 0;
|
||||
const wouldOverflowAbove = overflowAbove > 0;
|
||||
|
||||
if (wouldOverflowRight && wouldOverflowLeft) {
|
||||
x = overflowRight > overflowLeft ? xOffset : windowWidth - xOffset - tooltipWidth;
|
||||
} else if (wouldOverflowRight) {
|
||||
x = xPos - xOffset - tooltipWidth;
|
||||
} else {
|
||||
x = xPos + xOffset;
|
||||
}
|
||||
|
||||
if (wouldOverflowBelow && wouldOverflowAbove) {
|
||||
y = overflowBelow > overflowAbove ? yOffset : windowHeight - yOffset - tooltipHeight;
|
||||
} else if (wouldOverflowBelow) {
|
||||
y = yPos - yOffset - tooltipHeight;
|
||||
} else {
|
||||
y = yPos + yOffset;
|
||||
}
|
||||
return { x, y };
|
||||
};
|
Loading…
Reference in New Issue
Block a user