De Voorhoede

front-end developers

Progressive enhancement and accessibility in practice: adding functionality without excluding anyone

Building a progressively enhanced autocomplete field

We always like to build our projects with progressive enhancement in mind. Simply put we start with a basic experience and enhance it when possible. At one of our projects we got the request to create a search functionality which gives suggestions while the user starts typing a search query. This blog post describes how to do this by the book.

Start simple

The autocomplete component is essentially an <input type="search"> element. Only when the JavaScript features we need are supported, we add the extra functionality.

In our case we test the availability of our DOM helpers, such as querySelectorAll, and the Fetch API like so:

    const isSupported = dom().isSupported && ('fetch' in window);

Once this test passes we continue to instantiate the rest of the autocomplete component.

Describe states and roles for accessibility

To keep the autocomplete accessible we add various ARIA Roles to help user who use assistive technologies like screen readers for example. The markup for the input has additional ARIA attributes to give any assistive technology an indication something changed in another part of the page.

<input type="search"
  name="autocomplete-query"
    role="combobox"
    aria-expanded="false"
    aria-autocomplete="list"
    aria-owns="autocomplete-list"
    aria-activedescendant=""
    ...>

The search results get displayed in an <ol> element. This element has an ID attribute of autocomplete-list which is the value of the aria-owns attribute, connecting these elements. When a user types a search query the results get displayed and the aria-expanded attribute will change to “true”. Lastly, the aria-activedescendant will get the ID of the currently active result in the results list.

Add Keyboard support

To make the autocomplete component feel intuitive we need to make sure the up, down, enter, and escape keys are working as expected. When the autocomplete list is filled with suggestions, pressing the up and down arrow keys should move the focus up and down, respectively. Pressing enter will close the results and put the selected result in the input field, while the escape key closes the result list and clears the input field.

To accomplish this we use the keyup event on the input search field and check which keys are pressed with a switch statement.

dom(this.input).on('keyup', (event) => {
    switch (event.keyCode) {
        case KEY_CODE.UP:
            this.selectPrevItem();
            break;
        case KEY_CODE.DOWN:
            this.selectNextItem();
            break;
        case KEY_CODE.ENTER:
            this.close();
            break;
        default:
            this.open();
            this.resetSelection();
            this.search();
    }
});

Pressing the escape key when focusing an input field removes the content, which in our case hides the result list and yields the desired behaviour.

In conclusion

By starting with the bare functionality any user can still complete their task. Only when feature tests point out that we can execute the code for our component do we instantiate it. Additionally we make sure screen readers and other assistive technologies can make sense of our component by using ARIA roles, states and properties. This way we leave no-one behind when adding more functionality.