Fix histogram tooltip z-index issues

Closes #11950
pull/12348/head
Christopher Henn 2019-03-04 16:50:47 -08:00 committed by Chris Henn
parent f8c6fdbed7
commit fe04ef3476
9 changed files with 84 additions and 73 deletions

View File

@ -4,6 +4,7 @@ import RightClickLayer from 'src/clockface/components/right_click_menu/RightClic
import Nav from 'src/pageLayout'
import Notifications from 'src/shared/components/notifications/Notifications'
import LegendPortal from 'src/shared/components/LegendPortal'
import TooltipPortal from 'src/shared/components/TooltipPortal'
interface Props {
children: ReactChildren
@ -15,6 +16,7 @@ const App: SFC<Props> = ({children}) => (
<RightClickLayer />
<Nav />
<LegendPortal />
<TooltipPortal />
{children}
</div>
)

View File

@ -98,8 +98,6 @@ export const Histogram: SFC<Props> = ({
/>
{hoveredRowIndices && (
<HistogramTooltip
hoverX={hoverX}
hoverY={hoverY}
hoveredRowIndices={hoveredRowIndices}
layer={layer}
tooltip={tooltip}

View File

@ -1,82 +1,24 @@
import React, {useRef, SFC} from 'react'
import {SFC} from 'react'
import {HistogramTooltipProps, HistogramLayer} from 'src/minard'
import {useLayoutStyle} from 'src/minard/utils/useLayoutStyle'
import {useMousePos} from 'src/minard/utils/useMousePos'
import {getHistogramTooltipProps} from 'src/minard/utils/getHistogramTooltipProps'
const MARGIN_X = 15
const MARGIN_Y = 10
interface Props {
hoverX: number
hoverY: number
tooltip?: (props: HistogramTooltipProps) => JSX.Element
layer: HistogramLayer
hoveredRowIndices: number[] | null
}
const HistogramTooltip: SFC<Props> = ({
hoverX,
hoverY,
tooltip,
layer,
hoveredRowIndices,
}: Props) => {
const tooltipEl = useRef<HTMLDivElement>(null)
const {x: absoluteHoverX, y: absoluteHoverY} = useMousePos(document.body)
// Position the tooltip next to the mouse cursor, like this:
//
// ┌─────────────┐
// │ │
// (mouse) │ tooltip │
// │ │
// └─────────────┘
//
// The positioning is subject to the following restrictions:
//
// - If the tooltip overflows the right side of the screen, position it on
// the left side of the cursor instead
//
// - If the tooltip overflows the top or bottom of the screen (with a bit of
// margin), shift it just enough so that it is fully back inside the screen
//
useLayoutStyle(tooltipEl, ({offsetWidth, offsetHeight}) => {
let dx = MARGIN_X
let dy = 0 - offsetHeight / 2
if (absoluteHoverX + MARGIN_X + offsetWidth > window.innerWidth) {
dx = 0 - MARGIN_X - offsetWidth
}
if (absoluteHoverY + offsetHeight / 2 + MARGIN_Y > window.innerHeight) {
dy = 0 - (absoluteHoverY + offsetHeight - window.innerHeight) - MARGIN_Y
}
if (absoluteHoverY - offsetHeight / 2 - MARGIN_Y < 0) {
dy = 0 - absoluteHoverY + MARGIN_Y
}
return {
position: 'absolute',
left: `${hoverX + dx}px`,
top: `${hoverY + dy}px`,
}
})
if (!hoveredRowIndices) {
if (!hoveredRowIndices || !tooltip) {
return null
}
const tooltipProps = getHistogramTooltipProps(layer, hoveredRowIndices)
return (
<div className="minard-histogram-tooltip" ref={tooltipEl}>
{/* TODO: Provide a default tooltip implementation */}
{tooltip ? tooltip(tooltipProps) : null}
</div>
)
return tooltip(getHistogramTooltipProps(layer, hoveredRowIndices))
}
export default HistogramTooltip

View File

@ -19,6 +19,8 @@ export {
TooltipProps as HistogramTooltipProps,
} from 'src/minard/components/Histogram'
export {useTooltipStyle} from 'src/minard/utils/useTooltipStyle'
export {isNumeric} from 'src/minard/utils/isNumeric'
export type ColumnType = 'int' | 'uint' | 'float' | 'string' | 'time' | 'bool'

View File

@ -1,18 +1,18 @@
import {useLayoutEffect, MutableRefObject, CSSProperties} from 'react'
import {useLayoutEffect, CSSProperties} from 'react'
export const useLayoutStyle = (
ref: MutableRefObject<HTMLElement>,
el: HTMLElement,
f: (el: HTMLElement) => CSSProperties
) => {
useLayoutEffect(() => {
if (!ref.current) {
if (!el) {
return
}
const style = f(ref.current)
const style = f(el)
for (const [k, v] of Object.entries(style)) {
ref.current.style[k] = v
el.style[k] = v
}
})
}

View File

@ -0,0 +1,48 @@
import {useLayoutStyle} from 'src/minard/utils/useLayoutStyle'
import {useMousePos} from 'src/minard/utils/useMousePos'
const MARGIN_X = 15
export const useTooltipStyle = (el: HTMLDivElement) => {
const {x, y} = useMousePos(document.body)
// Position the tooltip next to the mouse cursor, like this:
//
// ┌─────────────┐
// │ │
// (mouse) │ tooltip │
// │ │
// └─────────────┘
//
// The positioning is subject to the following restrictions:
//
// - If the tooltip overflows the right side of the screen, position it on
// the left side of the cursor instead
//
// - If the tooltip overflows the top or bottom of the screen (with a bit of
// margin), shift it just enough so that it is fully back inside the screen
//
useLayoutStyle(el, ({offsetWidth, offsetHeight}) => {
let dx = MARGIN_X
let dy = 0 - offsetHeight / 2
if (x + dx + offsetWidth > window.innerWidth) {
dx = 0 - MARGIN_X - offsetWidth
}
if (y + dy + offsetHeight > window.innerHeight) {
dy -= y + dy + offsetHeight - window.innerHeight
}
if (y + dy < 0) {
dy += 0 - (y + dy)
}
return {
display: 'inline',
position: 'fixed',
left: `${x + dx}px`,
top: `${y + dy}px`,
}
})
}

View File

@ -9,6 +9,8 @@
padding: 10px;
color: $g14-chromium;
z-index: $z--dygraph-legend;
position: fixed;
display: none;
}
.histogram-tooltip--bin {

View File

@ -1,8 +1,11 @@
import React, {SFC} from 'react'
import React, {useRef, SFC} from 'react'
import {createPortal} from 'react-dom'
import {uniq, flatten} from 'lodash'
import {HistogramTooltipProps} from 'src/minard'
import {HistogramTooltipProps, useTooltipStyle} from 'src/minard'
import {format} from 'd3-format'
import {TOOLTIP_PORTAL_ID} from 'src/shared/components/TooltipPortal'
import 'src/shared/components/HistogramTooltip.scss'
const formatLarge = format('.4~s')
@ -10,12 +13,16 @@ const formatSmall = format('.4~g')
const formatBin = n => (n < 1 && n > -1 ? formatSmall(n) : formatLarge(n))
const HistogramTooltip: SFC<HistogramTooltipProps> = ({xMin, xMax, counts}) => {
const tooltipEl = useRef<HTMLDivElement>(null)
useTooltipStyle(tooltipEl.current)
const groupColNames = uniq(
flatten(counts.map(({grouping}) => Object.keys(grouping)))
).sort()
return (
<div className="histogram-tooltip">
return createPortal(
<div className="histogram-tooltip" ref={tooltipEl}>
<div className="histogram-tooltip--bin">
{formatBin(xMin)} &ndash; {formatBin(xMax)}
</div>
@ -49,7 +56,8 @@ const HistogramTooltip: SFC<HistogramTooltipProps> = ({xMin, xMax, counts}) => {
))}
</div>
</div>
</div>
</div>,
document.querySelector(`#${TOOLTIP_PORTAL_ID}`)
)
}

View File

@ -0,0 +1,9 @@
import React from 'react'
export const TOOLTIP_PORTAL_ID = 'tooltip-portal'
const TooltipPortal = () => {
return <div id={TOOLTIP_PORTAL_ID} />
}
export default TooltipPortal