react-spectrum
react-spectrum copied to clipboard
Fix useTagGroup set aria-live to 'polite' for better screen reader support
The current useTagGroup implementation has aria-live set to off, which means updates aren't announced when an input element is outside the grid, as seen in the APG example.
According to the specification, even if aria-live is set to 'off', screen readers will read the updates if the region is focused.
Indicates that updates to the region should not be presented to the user unless the user is currently focused on that region.
Therefore, setting aria-live: 'polite' only when focused seems unnecessary.
✅ Pull Request Checklist:
- [ ] Included link to corresponding React Spectrum GitHub Issue.
- [ ] Added/updated unit tests and storybook for this change (for new code or code which already has tests).
- [ ] Filled out test instructions.
- [ ] Updated documentation (if it already exists for this component).
- [ ] Looked at the Accessibility Practices for this feature - Aria Practices
📝 Test Instructions:
🧢 Your Project:
@snowystinger Thank you for your comment! It seems there might be a misunderstanding so, let me clarify the situation.
The APG example does use a visually-hidden span with aria-live="polite", aria-relevant="text" to announce changes, which is different from our implementation. When a tag is added, the span's text updates, e.g., "{tagName} added. {totalNumber} recipients total," and due to aria-relevant="text" the last added element is announced.
In our current React Aria tagGroup, aria-live='polite' is only set when the grid is focused, which may not be effective. To ensure announcements occur like in the APG example triggered by external actions such as clicking the add button, regardless of focus aria-live='polite' should always be applied, not just when focused.
Thanks, I did some digging, and this particular line of code is 6+ years old. I'll see if anyone can remember why we have it.
I tend to be careful with leaving static aria-live="polite" attributes on things. In this case, if the user isn’t actively interacting with the TagList, I think we want to avoid out-of-band live region announcement of new Tags. This sort of logic was added to preemptively avoid the sort of out of band announcement of content marked with aria-live="polite" that would overflow the VoiceOver buffer and interfere with a user's ability to read content in other tabs when a W3C spec was open in a Chrome browser tab:
- https://github.com/w3c/respec/issues/1456
- https://github.com/w3c/aria-practices/issues/715
Thanks @majornista for your expertise.
So in summary, theoretically setting it to 'off' would be the way to go, and then we don't need to check if focus is within. I don't know how good the support is for that, however, @ryo-manba we can test it if you'd like to make the change.
For components which are composite with a TagGroup in them, such as the APG example, users can follow the example as a blueprint. Our TagGroup's announcements won't happen while focus is in a textfield, so adding a live region which behaves how the APG example does would not conflict.
Thank you, @majornista for highlighting the caution needed with static aria-live="polite".
As @snowystinger mentioned, following the APG example might be a viable approach. If we opt for aria-live="off", then settings like aria-atomic and aria-relevant would become unnecessary.
However, the TagGroup documentation notes:
Accessible – Follows the ARIA grid pattern, with additional selection announcements via an ARIA live region.
So, if we're considering adapting the announcement behavior, I believe we should test the approach I suggested.
export const ExternalControl: TagGroupStory = {
render: () => (
<App />
)
};
let index = 2;
let App = () => {
let [items, setItems] = useState<Array<{key: string, label: string}>>([{key: '1', label: 'Cool Tag 1'}, {key: '2', label: 'Cool Tag 2'}])
let onRemoveFirst = () => {
setItems(prev => prev.slice(1));
};
let onAddNew = () => {
setItems(prev => {
index++;
return [...prev, {key: index, label: `Cool Tag ${index}`}];
});
};
return (
<>
<TagGroup aria-label="Tag group with icons" items={items}>
{(item: any) => (
<Item key={item.key} textValue={item.label}>
<Audio />
<Text>{item.label}</Text>
</Item>
)}
</TagGroup>
<Button variant="primary" onPress={onRemoveFirst}>Remove first</Button>
<Button variant="secondary" onPress={onAddNew}>Add New</Button>
</>
);
}
You can try it with this story in the React Spectrum component stories. You'll notice that the newly added tag is announced regardless of focus being outside the taggroup, which is not what we want.
@snowystinger Thanks for the code sample, I tested it out.
With the existing code, newly added tags are not announced when the focus is outside the TagGroup because aria-live: polite is only active when the grid is focused.
https://github.com/user-attachments/assets/242d0e40-86c5-4997-8d5e-1cc4ac431283
With the suggested code, newly added tags are announced regardless of whether the TagGroup is focused or not.
https://github.com/user-attachments/assets/96d0a4df-2d56-41dc-8cc0-360bfbf539c8
I used Assistiv Labs with Chrome 126 and NVDA for testing.
Right, that is the behavior we do NOT want. Tags added or removed should only be announced when you are in the group. If you have a composite control, then you can add your own announcer to handle that, such as is shown in the APG example.
Oh, I see, I misunderstood. Instead off setting polite on the tagGroup itself, we should implement a custom announcer. Thank you for the detailed response and your patience. I'll go ahead and close this PR.