Bootstrap keyboard navigable dropdown list with AngularJS

This time I comes with a problem I’ve faced in a project I’m currently working. We have some groups of Bootstrap components that mix a button with a HTML unordered list (button + ul tags).

The problem was that the user was not able to use the keyboard (cursor keys) to navigate through the elements of the list, so for a big form like the ones we have, changing keyboard to mouse is annoying for the users. They must fill lot of information and is always faster to use the keyboard.

8767790

So we had something like this (code simplified and cleaned to remove own CSS classes and other not related stuff):

<div class="btn-group">
    <button type="button" class="btn btn-default dropdown-toggle btn-block" data-toggle="dropdown">
        <span><fmt:message key="generic.no.selection" /></span>
    </button>
    <ul class="dropdown-menu">
        <li ng-repeat="element in elements.getPossibleValues()">
            <a ng-click="doSomething()">element.code</a>
        </li>
    </ul>
</div>

There are several solutions to solve this situation. I started trying to solve it directly with JQuery using a ngController function that was triggered when the user clicked (with mouse or keyboard) the button. Then I tried to do it more generic and cleaner, so I implemented an AngularJS directive, so now other developers only need to add an attribute to the button tag:

/**
 * Binds an on click function that sets the focus on the first element of the dropdown next to
 * the element that has the attribute 'focus-on-first-dropdown-element'. 
 */
uiModule.directive("focusOnFirstDropdownElement", function() {
    return {
        restrict: 'A',
        link: function (scope, element) {
	    element.bind('click', 
            function() {
                // Selects the sibling UL that contains the dropdown elements
                var list = element.next();
                // Put the focus on the first LI (a) element after some ms. The timeout is needed to give time to
                // show the list. If the timeout is not setup, the focus goes to the input text box associated to the button
                setTimeout(function() {
                    list.find('a:first').focus();
                }, 100);
            });
        }
    };
});

Also some modifications to the HTML code must be done:

  1. HTML <ul> tag needs to have the attribute role=”menu” (see Bootstrap).
  2. HTML <a> tags within the <li> elements must have an href attribute.
  3. Button tag must use the directive focusOnFirstDropdownElement.

So the new HTML after the changes is:

<div class="btn-group">
    <button type="button" focus-on-first-dropdown-element class="btn btn-default dropdown-toggle btn-block" data-toggle="dropdown">
        <span><fmt:message key="generic.no.selection" /></span>
    </button>
    <ul class="dropdown-menu" role="menu">
        <li ng-repeat="element in elements.getPossibleValues()">
            <a href="" ng-click="doSomething()">element.code</a>
        </li>
    </ul>
</div>

After that, now the user is able to use the keyboard to use the dropdown list.

Enjoy!