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="= $h($_['contactsSearchUrl']) ?>"
value="= $h($_['attendees']) ?>"
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]));