Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 157 additions & 4 deletions components/dash-core-components/src/components/Tooltip.react.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useEffect, useRef, useState} from 'react';
import PropTypes from 'prop-types';

import _JSXStyle from 'styled-jsx/style'; // eslint-disable-line no-unused-vars
Expand All @@ -7,20 +7,167 @@ import _JSXStyle from 'styled-jsx/style'; // eslint-disable-line no-unused-vars
* A tooltip with an absolute position.
*/
const Tooltip = props => {
const {bbox, border_color, background_color, id, loading_state} = props;
const {
bbox,
border_color,
background_color,
id,
loading_state,
auto_direction_container: autoDirectionContainer,
} = props;
const is_loading = loading_state?.is_loading;
const show = props.show && bbox;
const tooltipElement = useRef();

const initialDirection =
props.direction === 'auto' ? 'right' : props.direction;
const [realDirection, setRealDirection] = useState(initialDirection);

const getRealHiddenPart = value => {
return value > 0 ? value : 0;
};

const calculateHiddenPart = (parentRect, tooltipRect) => {
const hiddenDirections = [
{
direction: 'left',
value: getRealHiddenPart(parentRect.left - tooltipRect.left),
},
{
direction: 'right',
value: getRealHiddenPart(tooltipRect.right - parentRect.right),
},
{
direction: 'top',
value: getRealHiddenPart(parentRect.top - tooltipRect.top),
},
{
direction: 'bottom',
value: getRealHiddenPart(
tooltipRect.bottom - parentRect.bottom
),
},
];

return hiddenDirections.reduce((sum, hidden) => sum + hidden.value, 0);
Comment on lines +31 to +52

@alexcjohnson alexcjohnson Feb 2, 2023

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one doesn't use direction, right? So could just be an array of 4 numbers, then

return sum(hiddenDirections); // import {sum} from 'ramda'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also we might want to normalize by the tooltip size in each direction, ie instead of

tooltipRect.bottom - parentRect.bottom

use

(tooltipRect.bottom - parentRect.bottom) / tooltipRect.height

Otherwise for example a short but wide tooltip might wind up on top or bottom even if it's mostly cut off and could be mostly visible on the left or right, where more pixels are cut off but that's a smaller fraction of its total area.

};

const setAutoDirection = () => {
if (!tooltipElement.current || props.direction !== 'auto' || !show) {
return;
}

let tooltipRect = tooltipElement.current?.getBoundingClientRect();
const parent = autoDirectionContainer
? document.querySelector(autoDirectionContainer)
: tooltipElement.current?.parentElement.parentElement.parentElement;

if (!parent) {
return;
}

const parentRect = parent?.getBoundingClientRect();

const bboxCenter = {
x: (bbox.x0 + bbox.x1) / 2,
y: (bbox.y0 + bbox.y1) / 2,
};

const tooltipCenter = {
x: (tooltipRect.left + tooltipRect.right) / 2,
y: (tooltipRect.top + tooltipRect.bottom) / 2,
};

const xDelta = tooltipCenter.x - bboxCenter.x;
const yDelta = tooltipCenter.y - bboxCenter.y;

tooltipRect = {
left: tooltipRect.left - xDelta,
right: tooltipRect.right - xDelta,
top: tooltipRect.top - yDelta,
bottom: tooltipRect.bottom - yDelta,
width: tooltipRect.width,
height: tooltipRect.height,
};

const rightDirectionTooltipRect = {
...tooltipRect,
left: tooltipRect.left + tooltipRect.width / 2,
right: tooltipRect.right + tooltipRect.width / 2,
};

const leftDirectionTooltipRect = {
...tooltipRect,
left: tooltipRect.left - tooltipRect.width / 2,
right: tooltipRect.right - tooltipRect.width / 2,
};

const topDirectionTooltipRect = {
...tooltipRect,
top: tooltipRect.top - tooltipRect.height / 2,
bottom: tooltipRect.bottom - tooltipRect.height / 2,
};

const bottomDirectionTooltipRect = {
...tooltipRect,
top: tooltipRect.top + tooltipRect.height / 2,
bottom: tooltipRect.bottom + tooltipRect.height / 2,
};

let directionsHiddenParts = [
{
direction: 'right',
value: calculateHiddenPart(
parentRect,
rightDirectionTooltipRect
),
},
{
direction: 'left',
value: calculateHiddenPart(
parentRect,
leftDirectionTooltipRect
),
},
{
direction: 'top',
value: calculateHiddenPart(parentRect, topDirectionTooltipRect),
},
{
direction: 'bottom',
value: calculateHiddenPart(
parentRect,
bottomDirectionTooltipRect
),
},
];

directionsHiddenParts = directionsHiddenParts.sort(
(a, b) => a.value - b.value
);

setRealDirection(directionsHiddenParts[0].direction);
};

useEffect(setAutoDirection, [
show,
props.direction,
bbox,
props.children,
autoDirectionContainer,
]);

return (
<>
<div className="dcc-tooltip-bounding-box">
<span
data-dash-is-loading={is_loading || undefined}
className={`hover hover-${props.direction}`}
className={`hover hover-${realDirection}`}
>
<span
id={id}
className={`hover-content ${props.className}`}
ref={tooltipElement}
style={props.style}
>
{is_loading ? (
Expand Down Expand Up @@ -220,7 +367,13 @@ Tooltip.propTypes = {
/**
* The side of the `bbox` on which the tooltip should open.
*/
direction: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
direction: PropTypes.oneOf(['top', 'right', 'bottom', 'left', 'auto']),

/**
* Query selector for container in which tooltip should be visible.
* If not set the tooltip`s parent element will be used.
*/
auto_direction_container: PropTypes.string,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine a common ask would be just to make sure the tooltip doesn't go off the screen viewport (which typically won't change mid-hover) - is there a way we could allow that?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact I guess there are three flavors you might want: just keep it on screen, just keep it in the specified container, or keep it in the intersection of the screen and the specified container.

Not sure what API would be best for this... maybe in addition to query selectors, auto_direction_container could accept either 'screen' or 'screen,<selector>' - the latter kind of looks like a selector for both "screen" and your container.

Also maybe 'window' is better than 'screen'?


/**
* Color of the tooltip border, as a CSS color string.
Expand Down