-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Autoposition for tooltip #2381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Autoposition for tooltip #2381
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
@@ -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); | ||
| }; | ||
|
|
||
| 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 ? ( | ||
|
|
@@ -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, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, Also maybe |
||
|
|
||
| /** | ||
| * Color of the tooltip border, as a CSS color string. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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, thenThere was a problem hiding this comment.
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
use
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.