Announce the suggestion list to screen readers

The invitee input had no combobox semantics: assistive
technology could not tell that the widget owns a listbox with
suggestions. Mark the input as role="combobox" with
aria-autocomplete="list" and an initial aria-expanded="false",
point aria-controls at the list and give the JS-built list a
matching id so the reference resolves.
This commit is contained in:
stefan.tollkuehn committed 2026-10-23 14:08:59 +02:00
1 parent 5bba3a93cd
commit a9b7fd11af
3 files changed
+20 -1

No files matched your search

+1
View File
@@ -30,6 +30,7 @@
} }
var list = document.createElement('ul'); var list = document.createElement('ul');
list.id = 'atocal-suggestions';
list.className = 'atocal-suggestions'; list.className = 'atocal-suggestions';
list.hidden = true; list.hidden = true;
list.setAttribute('role', 'listbox'); list.setAttribute('role', 'listbox');
+4
View File
@@ -140,6 +140,10 @@ $l = $_['l10n'];
id="atocal-attendees" id="atocal-attendees"
name="attendees" name="attendees"
type="text" type="text"
role="combobox"
aria-autocomplete="list"
aria-controls="atocal-suggestions"
aria-expanded="false"
list="atocal-contact-emails" list="atocal-contact-emails"
data-search-url="<?= $h($_['contactsSearchUrl']) ?>" data-search-url="<?= $h($_['contactsSearchUrl']) ?>"
value="<?= $h($_['attendees']) ?>" value="<?= $h($_['attendees']) ?>"
+15 -1
View File
@@ -37,7 +37,9 @@ async function settle(ms = DEBOUNCE_MS) {
beforeAll(async () => { beforeAll(async () => {
document.body.innerHTML = document.body.innerHTML =
'<span><input id="atocal-attendees" data-search-url="' + SEARCH_URL + '"></span>'; '<span><input id="atocal-attendees" data-search-url="' + SEARCH_URL + '"'
+ ' role="combobox" aria-autocomplete="list"'
+ ' aria-controls="atocal-suggestions" aria-expanded="false"></span>';
await import('../../js/invitees.js'); await import('../../js/invitees.js');
@@ -59,6 +61,18 @@ afterEach(async () => {
}); });
describe('invitee suggestions', () => { describe('invitee suggestions', () => {
it('marks the input as a combobox controlling the list', () => {
expect(input.getAttribute('role')).toBe('combobox');
expect(input.getAttribute('aria-autocomplete')).toBe('list');
/* aria-controls must resolve to the list built by the widget */
const controlled = document.getElementById(
input.getAttribute('aria-controls')
);
expect(controlled).toBe(list);
expect(input.getAttribute('aria-expanded')).toBe('false');
});
it('renders matching contacts', async () => { it('renders matching contacts', async () => {
fetch.mockReturnValue(response([BOB])); fetch.mockReturnValue(response([BOB]));