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:
Ashley Harrison 2021-07-12 16:53:53 +01:00 committed by GitHub
parent 863b412d54
commit b1d576c5da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 39 additions and 89 deletions

View File

@ -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%;
`,
};
};

View File

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