-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
fix(#2101): TooltipTrigger: Tooltip should remain hidden in the DOM to provide an IDREF for aria-describedby on trigger element #2102
Conversation
…o provide an IDREF for aria-describedby on trigger element TooltipTrigger: Tooltip should remain hidden in the DOM to provide an IDREF for aria-describedby on trigger element. In order for the Tooltip to announce consistently when navigating elements using JAWS's virtual cursor or NVDA's browse mode, the Tooltip that displays as an overlay in a portal when the trigger is either hovered or focused should remain hidden in the DOM to provide an IDREF for `aria-describedby` on trigger element. The `triggerProps` returned by `useTooltipTrigger` should always include `aria-describedby` referencing the tooltip by `id`, regardless of whether the tooltip is visible in the portal or hidden in the DOM. Currently, a tooltip is only rendered to the DOM when the TooltipTrigger `state.isOpen === true`. The tooltip and the `aria-describedby` attribute on its trigger get removed when `state.isOpen === false`. This means that the text of the tooltip is only announced when the user focuses or hovers the trigger, and is not available to screen reader users navigating using virtual cursor/browse mode. Keep the Tooltip in the DOM with a `hidden` attribute when the tooltip is not shown. This is important for improving screen reader accessibility. See https://codepen.io/Moiety/pen/LaPvWy for a positive example in development for WAI-ARIA 1.2 Authoring Practices: https://w3c.github.io/aria-practices/#tooltip | Software | Version(s) | | ---------------- | ---------- | | react-spectrum | @react-spectrum/tooltip@3.1.3, @react-aria/tooltip@3.1.2 | Browser | All | Operating System | All Adobe/Accessibility
b29d487
to
674b18a
Compare
Build successful! 🎉 |
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.
in general, I like the way this is done, and I still can't think of the reason why we didn't want it rendered when it was hidden
And the tooltip tells you more information. | ||
</Tooltip> | ||
)} | ||
<Tooltip hidden={!state.isOpen} state={state} {...tooltipProps}> |
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.
It looks non-breaking, but that there is work for anyone who copied our example :(
I don't know how we want to call this out in our release
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.
I think the difference for anyone who copied from the prior example would be that the aria-describedby
prop on the trigger referencing the Tooltip element will persist even when the Tooltip is not in the DOM. This minor failure would be imperceivable to assistive technology users, but might trigger warnings with automated accessibility testing.
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.
btw, hidden isn't actually needed, it's in state if it's needed inside the component
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.
In my tests with NVDA and JAWS, the Tooltip is not immediately announced when using the virtual cursor, so perhaps we decided that persisting the Tooltip with aria-describedby was a moot point.
yep! that was it!
With JAWS, INSERT+TAB or CAPS LOCK+TAB (on laptop) announces the button and tooltip for the button focused with the virtual cursor.
With NVDA INSERT+numpad5 or CAPS LOCK+SHIFT+O (on laptop) announces the button and tooltip for the button focused in browse mode. [hidden] attribute makes no difference.
So, in both cases, there is a way to access the tooltip info for an object that would not be available if the Tooltip and aria-describedby
attribute were not persistent in the DOM.
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.
btw, hidden isn't actually needed, it's in state if it's needed inside the component
[hidden]
is necessary, because the tooltipProps
returned by useTooltip
do not return anything related to the isOpen
state:
react-spectrum/packages/@react-aria/tooltip/src/useTooltip.ts
Lines 37 to 41 in 622e33d
return { | |
tooltipProps: mergeProps(domProps, hoverProps, { | |
role: 'tooltip' | |
}) | |
}; |
A Tooltip is visible by default, and needs to be so that we can do animations on hide and show.
I could put the isOpen
logic in the Tooltip component for this example, but I think this may be easier to understand and is more consistent with how Tooltips work when using an Overlay.
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.
What I mean is that it should be in the aria hook and should be returned from useTooltip, or am I missing some reason why it can't be?
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.
An alternative example adding a CSS animation to set opacity
and visibility
based on isOpen
state:
import {useTooltipTriggerState} from '@react-stately/tooltip';
import {mergeProps} from '@react-aria/utils';
function Tooltip({state, ...props}) {
let {tooltipProps} = useTooltip(props, state);
let {isOpen} = state;
return (
<span
style={{
position: 'absolute',
left: '5px',
top: '100%',
marginTop: '10px',
backgroundColor: 'white',
color: 'black',
padding: '5px',
/* fade in or fade out tooltip based on isOpen state */
opacity: (isOpen ? 1 : 0),
visibility: (isOpen ? 'visible' : 'hidden'),
transition: `
opacity 130ms ease-in-out 0ms,
visibility 0ms linear ${isOpen ? 0 : 130}ms
`
}}
{...mergeProps(props, tooltipProps)} >
{props.children}
</span>
);
}
function Example(props) {
let state = useTooltipTriggerState(props);
let ref = React.useRef();
// Get props for the trigger and its tooltip
let {triggerProps, tooltipProps} = useTooltipTrigger(props, state, ref);
return (
<span style={{position: 'relative'}}>
<button ref={ref} {...triggerProps}>I have a tooltip</button>
<Tooltip state={state} {...tooltipProps}>
And the tooltip tells you more information.
</Tooltip>
</span>
);
}
<Example />
<Example />
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.
Ok, I see, we technically have two tooltips... https://github.com/adobe/react-spectrum/pull/2102/files#diff-225c01ce8d5eeafee3ecca35c4f8cded18543869468adbfb9b956914bde5280bR74 and they are in different places
whereas this mdx file only has one
could we wrap the new hidden tooltip into a new component (HiddenTooltip) that calls useTooltip which would return hidden when state is closed? I don't think it'll matter if the hidden element has role=tooltip? otherwise we can remove the role when hidden. This way we don't have to move the overlay. It should also fix this example so the user only has to remove the {isOpen &&
the aria hooks should take care of everything for the user
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.
Ok I added a note to the description before the example about why hidden
is used here.
In my tests with NVDA and JAWS, the Tooltip is not immediately announced when using the virtual cursor, so perhaps we decided that persisting the Tooltip with |
yep! that was it! |
I am a little confused by this. The ticket says that rendering tooltips in the dom will help improve assistive technology but this last comment
makes it sound like it is not really necessary? If we can avoid having to add all tooltips to the DOM, I would prefer that. It has the potential of adding a lot of bloat if there are many tooltips on a page (maybe in a table) and would also require react-aria users to have to follow the same process themselves. |
Build successful! 🎉 |
I think it's best to have the Tooltips in the DOM.
When the Tooltip is omitted from the DOM, the screen reader has no way to access the Tooltip information in virtual cursor/browse mode. |
Build successful! 🎉 |
🐛 Bug Report
TooltipTrigger: Tooltip should remain hidden in the DOM to provide an IDREF for aria-describedby on trigger element.
🤔 Expected Behavior
In order for the Tooltip information to be available to assistive technology when navigating elements using JAWS's virtual cursor or NVDA's browse mode, the Tooltip that displays as an overlay in a portal when the trigger is either hovered or focused should remain hidden in the DOM to provide an IDREF for
aria-describedby
on trigger element. ThetriggerProps
returned byuseTooltipTrigger
should always includearia-describedby
referencing the tooltip byid
, regardless of whether the tooltip is visible in the portal or hidden in the DOM.😯 Current Behavior
Currently, a tooltip is only rendered to the DOM when the TooltipTrigger
state.isOpen === true
. The tooltip and thearia-describedby
attribute on its trigger get removed whenstate.isOpen === false
. This means that the text of the tooltip is only announced when the user focuses or hovers the trigger, and is not available to screen reader users navigating using virtual cursor/browse mode.💁 Possible Solution
Keep the Tooltip in the DOM with a
hidden
attribute when the tooltip is not shown.🔦 Context
This is important for improving screen reader accessibility.
💻 Code Sample
See https://codepen.io/Moiety/pen/LaPvWy for a positive example in development for WAI-ARIA 1.2 Authoring Practices: https://w3c.github.io/aria-practices/#tooltip
🌍 Your Environment
Closes #2101
✅ Pull Request Checklist:
📝 Test Instructions:
🧢 Your Project:
Adobe/Accessibility