From b1d576c5dac58b1eb46fc940ac48395b818fcea7 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 12 Jul 2021 16:53:53 +0100 Subject: [PATCH] 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 --- .../src/components/VizTooltip/VizTooltip.tsx | 25 +---- .../VizTooltip/VizTooltipContainer.tsx | 103 ++++++------------ 2 files changed, 39 insertions(+), 89 deletions(-) diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltip.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltip.tsx index c5324bfa8aa..816038b817e 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltip.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltip.tsx @@ -1,10 +1,7 @@ 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 @@ -49,30 +46,14 @@ export interface VizTooltipProps { * @public */ export const VizTooltip: React.FC = ({ content, position, offset }) => { - const styles = useStyles(getStyles); if (position) { return ( - - - {content} - - + + {content} + ); } return null; }; VizTooltip.displayName = 'VizTooltip'; - -const getStyles = () => { - return { - portal: css` - position: absolute; - top: 0; - left: 0; - pointer-events: none; - width: 100%; - height: 100%; - `, - }; -}; diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContainer.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContainer.tsx index 411693e39cb..137045fc706 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContainer.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContainer.tsx @@ -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 { useStyles2 } from '../../themes'; import { getTooltipContainerStyles } from '../../themes/mixins'; -import useWindowSize from 'react-use/lib/useWindowSize'; -import { Dimensions2D, GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2 } from '@grafana/data'; +import { usePopper } from 'react-popper'; /** * @public @@ -24,78 +24,47 @@ export const VizTooltipContainer: React.FC = ({ className, ...otherProps }) => { - const tooltipRef = useRef(null); - const [tooltipMeasurement, setTooltipMeasurement] = useState({ 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.height, tooltipMeasurement.width] + const [tooltipRef, setTooltipRef] = useState(null); + const virtualElement = useMemo( + () => ({ + getBoundingClientRect() { + return { top: positionY, left: positionX, bottom: positionY, right: positionX, width: 0, height: 0 }; + }, + }), + [positionY, positionX] ); - - useLayoutEffect(() => { - if (tooltipRef.current) { - resizeObserver.observe(tooltipRef.current); - } - - return () => { - resizeObserver.disconnect(); - }; - }, [resizeObserver]); - - // Make sure tooltip does not overflow window - useLayoutEffect(() => { - let xO = 0, - yO = 0; - if (tooltipRef && tooltipRef.current) { - 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: 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 styles = useStyles2(getStyles); return (