← All Articles A Product of Kinsa Creative

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:


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

Feedback?

Email us at enquiries@kinsa.cc.