From a9b7fd11af50d48c290d1d3ab1396b6ef69056d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tollk=C3=BChn?= Date: Fri, 23 Oct 2026 14:08:59 +0200 Subject: [PATCH] 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. --- js/invitees.js | 1 + templates/confirm.php | 4 ++++ tests/js/invitees.test.mjs | 16 +++++++++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/js/invitees.js b/js/invitees.js index b986f14..1a52edc 100644 --- a/js/invitees.js +++ b/js/invitees.js @@ -30,6 +30,7 @@ } var list = document.createElement('ul'); + list.id = 'atocal-suggestions'; list.className = 'atocal-suggestions'; list.hidden = true; list.setAttribute('role', 'listbox'); diff --git a/templates/confirm.php b/templates/confirm.php index da5db80..704f634 100644 --- a/templates/confirm.php +++ b/templates/confirm.php @@ -140,6 +140,10 @@ $l = $_['l10n']; id="atocal-attendees" name="attendees" type="text" + role="combobox" + aria-autocomplete="list" + aria-controls="atocal-suggestions" + aria-expanded="false" list="atocal-contact-emails" data-search-url="" value="" diff --git a/tests/js/invitees.test.mjs b/tests/js/invitees.test.mjs index bc5ea3b..52c0378 100644 --- a/tests/js/invitees.test.mjs +++ b/tests/js/invitees.test.mjs @@ -37,7 +37,9 @@ async function settle(ms = DEBOUNCE_MS) { beforeAll(async () => { document.body.innerHTML = - ''; + ''; await import('../../js/invitees.js'); @@ -59,6 +61,18 @@ afterEach(async () => { }); 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 () => { fetch.mockReturnValue(response([BOB]));