Tooltip and Focusable not working as expected
Provide a general summary of the issue here
I am replacing our legacy tooltip with a React ARIA tooltip. To avoid manually refactoring our entire app, I created a backwards-compatible tooltip for our component library. This way, everything should work.
The problem is that wrapping my children with Focusable is not enough; I also need forwardRef, tabIndex, and role= "button" to be included in the div for the wrapper to work properly.
In order to avoid conflicts with other buttons, I created a function, isElementFocusable to decide if we're going to wrap or not. I am wondering if there is a less hacky way to do this with Focusable taking care of things under the hood for users.
Here is the code for the component if you have any tips.
import React, { ReactNode } from 'react'
import { TooltipTrigger, Tooltip as AriaTooltip, Focusable, TooltipProps } from 'react-aria-components'
import { PlacementType } from '../types'
import { cn, isElementFocusable, mapPlacementToAriaPlacement } from '../waterfallDSUtils'
export interface WaterfallTooltipProps extends Omit<TooltipProps, 'placement' | 'children'> {
/** Children that will be wrapped by tooltip */
children?: ReactNode
/**
* Additional classes to style wrapped component,
* components are only wrapped when non-focusable (ex div, span etc).
* This is mostly to mitigate styling issues that the wrapper might cause.
* Buttons and DOM focusable elements will not receive className since they don't get wrapped.
* Feel free to apply styles them directly instead.
* */
className?: string
/** Content to show inside tooltip */
content?: string | ReactNode
/** Test ID for testing purposes */
dataTestid?: string
/** Disable tooltip */
isDisabled?: boolean
/** Control tooltip visibility (controlled mode) */
isOpen?: boolean
/** Where to place tooltip in relation to object. */
placement?: PlacementType
}
const Title = ({ children }: { children?: ReactNode }): React.JSX.Element => (
<div className="text-xs text-default-50">{children}</div>
)
const Text = ({ children }: { children?: ReactNode }): React.JSX.Element => (
<div className="text-xs text-default-500">{children}</div>
)
const RefWrapper = React.forwardRef<HTMLDivElement, { children: ReactNode; className?: string }>(
({ children, className }, ref) => (
<Focusable>
<div
ref={ref}
tabIndex={0}
role="button"
className={cn('flex cursor-default items-center justify-center', className)}
>
{children}
</div>
</Focusable>
)
)
export const Tooltip = ({
children,
className,
content,
dataTestid,
isDisabled,
isOpen,
placement = 'top',
...props
}: WaterfallTooltipProps): React.JSX.Element => {
if (isDisabled || !content) return <>{children}</>
return (
<TooltipTrigger delay={300} closeDelay={0} isOpen={isOpen}>
{isElementFocusable(children) ? (
// Pass focusable components directly
children
) : (
// Wrap non-focusable elements in Focusable
<RefWrapper className={className}>
{React.isValidElement(children) ? children : <span>{children}</span>}
</RefWrapper>
)}
<AriaTooltip
placement={mapPlacementToAriaPlacement(placement)}
className={cn('react-aria-Tooltip', 'p-s')}
data-wds-component="wds-tooltip"
data-testid={dataTestid}
{...props}
>
{content}
</AriaTooltip>
</TooltipTrigger>
)
}
Tooltip.Title = Title
Tooltip.Text = Text
๐ค Expected Behavior?
I would expect Focusable to take care of making the component focusable without additional wrappers making the tooltip work out of the box.
๐ฏ Current Behavior
Focusable doesn't work for me unless I provide with a div with tabIndex button role and forwardRef
<Focusable>
<div
ref={ref}
tabIndex={0}
role="button"
className={cn('flex cursor-default items-center justify-center', className)}
>
{children}
</div>
</Focusable>
๐ Possible Solution
Focusable to be the div that handles hoverability? or make the TooltipTrigger work for non-focusable elements as well?
๐ฆ Context
This is used in a component library internally which gets bundled and used in our applications
๐ฅ๏ธ Steps to Reproduce
Wrap TooltipTrigger around a non-focusable element. Add Focusable around that non-focusable element. Tooltip doesn't work.
Version
latest
What browsers are you seeing the problem on?
Chrome
If other, please specify.
No response
What operating system are you using?
macOs
๐งข Your Company/Team
No response
๐ท Tracking Issue
No response
You could wrap the component a little differently so that you can forward all the props instead of applying them manually?
https://react-spectrum.adobe.com/react-aria/Tooltip.html#custom-trigger see the code just under
Note that any <Focusable> child must have an ARIA role or use an appropriate semantic HTML element so that screen readers can announce the content of the tooltip. Trigger components must forward their ref and spread all props to a DOM element.
It's a bit of an inversion of what you have. Hopefully that simplifies things for you.
Focusable does set a tabIndex automatically, but you need to set a role yourself since there's not always an obvious default. And yes you must forward the ref to the DOM element for this to work - we need access to the DOM element in order to position the tooltip correctly.