mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TimeSeries: Improve tooltip positioning when tooltip overflows (#36440)
* TimeSeries: Improve tooltip positioning when tooltip overflows * VizTooltip: Use react-popper, extract positioning calculation into util function + add unit tests * VizTooltip: Keep ref as tooltipRef * Use popper only for VizTooltip positioning * VizTooltip: Set altAxis to true to prevent overflow on y axis Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
parent
863b412d54
commit
b1d576c5da
@ -1,10 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { css } from '@emotion/css';
|
|
||||||
import { Portal } from '../Portal/Portal';
|
|
||||||
import { Dimensions, TimeZone } from '@grafana/data';
|
import { Dimensions, TimeZone } from '@grafana/data';
|
||||||
import { FlotPosition } from '../Graph/types';
|
import { FlotPosition } from '../Graph/types';
|
||||||
import { VizTooltipContainer } from './VizTooltipContainer';
|
import { VizTooltipContainer } from './VizTooltipContainer';
|
||||||
import { useStyles } from '../../themes';
|
|
||||||
import { TooltipDisplayMode } from './models.gen';
|
import { TooltipDisplayMode } from './models.gen';
|
||||||
|
|
||||||
// Describes active dimensions user interacts with
|
// Describes active dimensions user interacts with
|
||||||
@ -49,30 +46,14 @@ export interface VizTooltipProps {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export const VizTooltip: React.FC<VizTooltipProps> = ({ content, position, offset }) => {
|
export const VizTooltip: React.FC<VizTooltipProps> = ({ content, position, offset }) => {
|
||||||
const styles = useStyles(getStyles);
|
|
||||||
if (position) {
|
if (position) {
|
||||||
return (
|
return (
|
||||||
<Portal className={styles.portal}>
|
<VizTooltipContainer position={position} offset={offset || { x: 0, y: 0 }}>
|
||||||
<VizTooltipContainer position={position} offset={offset || { x: 0, y: 0 }}>
|
{content}
|
||||||
{content}
|
</VizTooltipContainer>
|
||||||
</VizTooltipContainer>
|
|
||||||
</Portal>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
VizTooltip.displayName = 'VizTooltip';
|
VizTooltip.displayName = 'VizTooltip';
|
||||||
|
|
||||||
const getStyles = () => {
|
|
||||||
return {
|
|
||||||
portal: css`
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { useState, useLayoutEffect, useRef, HTMLAttributes, useMemo } from 'react';
|
import React, { useState, HTMLAttributes, useMemo } from 'react';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { useStyles2 } from '../../themes';
|
import { useStyles2 } from '../../themes';
|
||||||
import { getTooltipContainerStyles } from '../../themes/mixins';
|
import { getTooltipContainerStyles } from '../../themes/mixins';
|
||||||
import useWindowSize from 'react-use/lib/useWindowSize';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Dimensions2D, GrafanaTheme2 } from '@grafana/data';
|
import { usePopper } from 'react-popper';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -24,78 +24,47 @@ export const VizTooltipContainer: React.FC<VizTooltipContainerProps> = ({
|
|||||||
className,
|
className,
|
||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
const [tooltipRef, setTooltipRef] = useState<HTMLDivElement | null>(null);
|
||||||
const [tooltipMeasurement, setTooltipMeasurement] = useState<Dimensions2D>({ width: 0, height: 0 });
|
const virtualElement = useMemo(
|
||||||
const { width, height } = useWindowSize();
|
() => ({
|
||||||
const [placement, setPlacement] = useState({
|
getBoundingClientRect() {
|
||||||
x: positionX + offsetX,
|
return { top: positionY, left: positionX, bottom: positionY, right: positionX, width: 0, height: 0 };
|
||||||
y: positionY + offsetY,
|
},
|
||||||
});
|
}),
|
||||||
|
[positionY, positionX]
|
||||||
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.height, tooltipMeasurement.width]
|
|
||||||
);
|
);
|
||||||
|
const { styles: popperStyles, attributes } = usePopper(virtualElement, tooltipRef, {
|
||||||
useLayoutEffect(() => {
|
placement: 'bottom-start',
|
||||||
if (tooltipRef.current) {
|
modifiers: [
|
||||||
resizeObserver.observe(tooltipRef.current);
|
{ name: 'arrow', enabled: false },
|
||||||
}
|
{
|
||||||
|
name: 'preventOverflow',
|
||||||
return () => {
|
enabled: true,
|
||||||
resizeObserver.disconnect();
|
options: {
|
||||||
};
|
altAxis: true,
|
||||||
}, [resizeObserver]);
|
rootBoundary: 'viewport',
|
||||||
|
},
|
||||||
// Make sure tooltip does not overflow window
|
},
|
||||||
useLayoutEffect(() => {
|
{
|
||||||
let xO = 0,
|
name: 'offset',
|
||||||
yO = 0;
|
options: {
|
||||||
if (tooltipRef && tooltipRef.current) {
|
offset: [offsetX, offsetY],
|
||||||
const xOverflow = width - (positionX + tooltipMeasurement.width);
|
},
|
||||||
const yOverflow = height - (positionY + tooltipMeasurement.height);
|
},
|
||||||
if (xOverflow < 0) {
|
],
|
||||||
xO = tooltipMeasurement.width;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (yOverflow < 0) {
|
|
||||||
yO = tooltipMeasurement.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPlacement({
|
|
||||||
x: positionX + offsetX - xO,
|
|
||||||
y: positionY + offsetY - yO,
|
|
||||||
});
|
|
||||||
}, [width, height, positionX, offsetX, positionY, offsetY, tooltipMeasurement.width, tooltipMeasurement.height]);
|
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={tooltipRef}
|
ref={setTooltipRef}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
...popperStyles.popper,
|
||||||
left: 0,
|
display: popperStyles.popper?.transform ? 'block' : 'none',
|
||||||
top: 0,
|
transition: 'all ease-out 0.2s',
|
||||||
transform: `translate3d(${placement.x}px, ${placement.y}px, 0)`,
|
|
||||||
transition: 'all ease-out 0.1s',
|
|
||||||
}}
|
}}
|
||||||
|
{...attributes.popper}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
className={cx(styles.wrapper, className)}
|
className={cx(styles.wrapper, className)}
|
||||||
>
|
>
|
||||||
|
Loading…
Reference in New Issue
Block a user