Keyboard accessible navigation for a search result listbox
Given a site search widget that consists of a search input field with AJAX functionality to perform the search and return the results, a button to explicitly execute the search, and a listbox to present the results of the search in, based on the A11Y guidelines for a listbox, the following keyboard functionality needs to be programmed:
- After keying a value into the search input field, with the focus still on the search input field, hitting the return/enter key executes the search
- After keying a value into the search input field and then tabbing the focus to the search button, hitting the return/enter key executes the search
- After keying a value into the search input field and allowing for results to be returned, the up and down arrows will cycle through the results list in an upwards or downwards direction, respectively
- With an item in the results list selected, hitting the return/enter key navigates to that result
- After keying a value into the search input field and allowing for results to be returned, the escape key should collapse the list and return focus to the search input field
- After keying a value into the search input field and allowing for results to be returned, the home key should select the first item in the results list
- After keying a value into the search input field and allowing for results to be returned, the end key should select the last item in the results list
document.addEventListener('DOMContentLoaded', function () {
const inputWidget = document.getElementById('search-input');
const resultsContainer = document.getElementById('search-results');
const searchButton = document.getElementById('search-btn');
function focusAndSetActiveStateOnSearchResult(element, inputWidget, resultsContainer) {
/* Applies focus by setting aria-selected=true on an element (element) in the results container list (resultsContainer) while applying the value of the selected element to the search input widget (inputWidget) */
if (element && inputWidget && resultsContainer) {
for (let child of resultsContainer.children) {
child.setAttribute('aria-selected', 'false');
}
element.setAttribute('aria-selected', 'true');
inputWidget.value = element.textContent;
}
}
function handleSearchInputFieldKeyUp(event) {
if (inputWidget.value.trim() !== '') {
resultsContainer.setAttribute('aria-hidden', 'false');
handleSearch(inputWidget, resultsContainer); // stubbed function to perform the search and return results
} else {
resultsContainer.setAttribute('aria-hidden', 'true');
}
}
// To clear the keyup event listener
function clearSearchInputFieldKeyUpListener() {
inputWidget.removeEventListener('keyup', handleSearchInputFieldKeyUp);
}
function handleSearchInputFieldKeyDown(event) {
clearSearchInputFieldKeyUpListener();
const activeItem = resultsContainer.querySelector('[aria-selected="true"]');
if (event.keyCode === 27) { // escape key
event.preventDefault();
resultsContainer.setAttribute('aria-hidden', 'true');
} else if (event.keyCode === 36) { // home key
event.preventDefault();
focusAndSetActiveStateOnSearchResult(resultsContainer.firstChild, inputWidget, resultsContainer);
} else if (event.keyCode === 35) { // end key
event.preventDefault();
focusAndSetActiveStateOnSearchResult(resultsContainer.lastChild, inputWidget, resultsContainer);
} else if (event.keyCode === 40) { // down key
event.preventDefault();
if (activeItem && activeItem.nextElementSibling) {
focusAndSetActiveStateOnSearchResult(activeItem.nextElementSibling, inputWidget, resultsContainer);
} else {
focusAndSetActiveStateOnSearchResult(resultsContainer.firstChild, inputWidget, resultsContainer);
}
} else if (event.keyCode === 38) { // up key
event.preventDefault();
if (activeItem && activeItem.previousElementSibling) {
focusAndSetActiveStateOnSearchResult(activeItem.previousElementSibling, inputWidget, resultsContainer);
} else {
focusAndSetActiveStateOnSearchResult(resultsContainer.lastChild, inputWidget, resultsContainer);
}
} else if (event.keyCode === 13) { // return/enter key
event.preventDefault();
if (activeItem) {
// the handleSearch() function adds a click event listener to the list item to handle navigation to that item
activeItem.click();
} else {
handleSearch(true, inputWidget, resultsContainer);
}
} else {
// We use the keyUp event listener so as to perform the search after the character has been keyed in
inputWidget.addEventListener('keyup', handleSearchInputFieldKeyUp);
}
}
// Beause we want to prevent the default event, we use keyDown
inputWidget.addEventListener('keydown', handleSearchInputFieldKeyDown);
searchButton.addEventListener('click', (event) => {
handleSearch(true, inputWidget, resultsContainer);
});
searchButton.addEventListener('keydown', (event) => {
if (event.keyCode === 13) { // return/enter key
event.preventDefault();
const activeItem = resultsContainer.querySelector('[aria-selected="true"]');
// the handleSearch() function adds a click event listener to the list item to handle navigation to that item
if (activeItem) {
activeItem.click();
} else {
handleSearch(true, inputWidget, resultsContainer);
}
}
});
});
Resources
- Orange.com listbox development tutorial
- The Mozilla developer docs for listbox
- The W3C Listbox Pattern documentation
Feedback?
Email us at enquiries@kinsa.cc.