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".
But I still want dynamic positioning with collision detection.
I think either
The latter is preferred, I think.
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:
Most people wanting tooltips in web apps.
Most other tooltip libs provide out-of-the-box solutions for rendering tooltips. See Reakit: https://reakit.io/docs/tooltip/.
Complete gist of my modified code: https://gist.github.com/brookback/67bb0de295c8af96f50ce6bec8cf4cd8.
Thanks for a great, hackable library! <3
Thank you for opening this issue
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
tooltipRecttriggerRectin order to calculate collisions and heights. The current API doesn't expose tooltipRect (if I'm not mistaken).
The current API doesn't expose tooltipRect (if I'm not mistaken).
There's a small chance that you are mistaken! Here's an example of one that I implemented. Mostly just copy/pasted from the docs with a few modifications.
It looks like the the position callback provides both triggerRect and tooltipRect as arguments to the function that you pass in.
@graygilmore Yes, that's correct!
Interesting approach, thanks. No measuring at all of the tooltipRect. Is that code handling where the tooltip is above an element? Like, does it adjust the position and rotation of the triangle?
For this approach we assume that the tooltip always will appear below the hovered item. So the only thing we need to be aware about are left/right boundaries. There's some CSS love happening behind the scenes that doesn't show the whole picture but hopefully having tooltipRect in the position callback gets you what you need!
Most helpful comment
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.