Better ergonomics around showing arrows in Tooltips
See original GitHub issueš Feature request
Current Behavior
At the time of writing, itās not entirely intuitive to support arrows on tooltips (which is a fairly common use case). The docs suggest I need to drop down to low-level useTooltip
APIs and place the tooltip myself.
The docs tell me to do my own position
function to center the tooltips to ākeep the positioning logic simplerā.
Desired Behavior
But I still want dynamic positioning with collision detection.
I think either
- the docs should show how to position tooltips with arrows with the current API, or
- the library itself should support arrows out of the box and handle the positioning (perhaps opt-in through a prop, I donāt know).
The latter is preferred, I think.
Suggested Solution
Right now, I inlined the tooltips.tsx
source into our app and hacked the Reach Tooltip code to render an arrow div
along with the main tooltip.
export type TooltipContentProps = {
ariaLabel?: string;
position?: Position;
/** Provide an optional positioning function for the arrow. */
+ positionArrow?: Position;
label: React.ReactNode;
isVisible?: boolean;
triggerRect: DOMRect | null;
};
function TooltipContent(
// ...
position = positionDefault,
+ positionArrow = positionArrowDefault,
We need to let the arrow know about collisions, so we can make it point downwards:
export type Collisions = {
left: boolean;
right: boolean;
bottom: boolean;
top: boolean;
};
const getCollisions = (
triggerRect: DOMRect,
tooltipRect: DOMRect
): Collisions => ({
top: triggerRect.top - tooltipRect.height < 0,
right: window.innerWidth < triggerRect.left + tooltipRect.width,
bottom:
window.innerHeight < triggerRect.bottom + tooltipRect.height + OFFSET,
left: triggerRect.left - tooltipRect.width < 0,
});
const positionArrow: Position = (triggerRect, tooltipRect, collisions) => {
if (!triggerRect || !tooltipRect || !collisions) {
return {};
}
const directionUp = collisions.bottom && !collisions.top;
return {
left: triggerRect.left - 10 + triggerRect.width / 2,
top: directionUp
? triggerRect.bottom + window.pageYOffset - tooltipRect.height - 10
: triggerRect.bottom + window.pageYOffset,
...(directionUp
? {
transform: 'rotate(180deg)',
}
: undefined),
};
};
Then add the arrow below the tooltip in TooltipContent
:
const collisions =
!!triggerRect && !!tooltipRect
? getCollisions(triggerRect, tooltipRect)
: undefined;
return (
<Fragment>
<Comp
data-reach-tooltip
role={useAriaLabel ? undefined : 'tooltip'}
id={useAriaLabel ? undefined : id}
children={label}
style={{
...style,
...(!tooltipRect
? {
visibility: 'hidden',
}
: position(triggerRect, tooltipRect, collisions)),
}}
ref={ref}
{...rest}
/>
<div
data-reach-tooltip-arrow
style={{
position: 'absolute',
width: 0,
height: 0,
...(!tooltipRect
? {
visibility: 'hidden',
}
: positionArrow(triggerRect, tooltipRect, collisions)),
}}
/>
{useAriaLabel && (
<VisuallyHidden role="tooltip" id={id}>
{ariaLabel}
</VisuallyHidden>
)}
</Fragment>
);
See complete code in the gist linked below.
Iām sure this can be made much cleaner, but I just needed to get the job done. I think the authors have a better starting point š
I imagine the API could be:
<Tooltip label="Foobar" arrow>
<button>Hey</button>
</Tooltip>
This suggests these additions to TooltipContentProps
:
export type TooltipContentProps = {
ariaLabel?: string;
position?: Position;
label: React.ReactNode;
isVisible?: boolean;
triggerRect: DOMRect | null;
// New š„
arrow?: boolean; // Default to false
positionArrow?: Position; // Default to a nice positioning function, but let users customise
};
Then the library code takes complete care about:
- positioning the tooltips with collision detection.
- positioning the arrows accordingly.
- providing custom positioning functions for both tooltip and arrows as a part of the external API.
Who does this impact? Who is this for?
Most people wanting tooltips in web apps.
Describe alternatives youāve considered
Most other tooltip libs provide out-of-the-box solutions for rendering tooltips. See Reakit: https://reakit.io/docs/tooltip/.
Additional context
Complete gist of my modified code: https://gist.github.com/brookback/67bb0de295c8af96f50ce6bec8cf4cd8.
Thanks for a great, hackable library! ā¤ļø
Issue Analytics
- State:
- Created 4 years ago
- Reactions:4
- Comments:7 (1 by maintainers)
Top GitHub Comments
Awesome write-up, thank you! As it stands I think this is still better handled by docs and examples than new props or abstract components. Reach UI is intended to be fairly low-level, and we want you to build your own abstractions on top of ours. Iāll take a closer look at your fork code to see if I canāt figure out better ways we might handle it either in the docs or some better example code.
Thanks for replying š !
Whilst I totally understand you donāt want to bog down the lib with arrow functionality, I think, as you say, something can be done to clarify how to get the best of both worlds: the built-in positioning and arrows that behave in a good way.
The problem I ran into when positioning my arrows (without resorting to centering the whole tooltip horizontally and disallowing collision detection) was that I needed references to
tooltipRect
triggerRect
in order to calculate collisions and heights. The current API doesnāt expose
tooltipRect
(if Iām not mistaken).