Working with Web Accessibility

Home / Working with Web Accessibility

Over the past few weeks, I’ve been working on making a legacy web application more accessible.  Key factors in accessibility are being keyboard and narrator friendly.  These factors are governed by various “A11yMAS” guidelines.  For this post, I’ll focus on a particular AngularJS datatimepicker (calendar) directive, which is used in the legacy web application, that lacked keyboard accessibility completely. 

The datetimepicker, in question, is here, if you’re interested. One of the major things that will make elements in a directive/control inaccessible are using semantic layout elements for interaction. In this particular directive, the calendar that is displayed for user interaction is rendered via an HTML TABLE and relies on TD and TH elements and click events for all interaction. As such, a keyboard-only user can’t access or interact with the elements within the calendar at all.

One quick fix, then, is to place typically interactive elements within each TD/TH like a button. This requires a bit of modification to the behavior of the directive and the template that the directive relies upon to render the calendar. We can take things a step further, though, and capture keypress events to allow the user to navigate through the calendar table using arrow keys.

As a simplified example, the basic calendar renders similarly to the below code block.

<table>
  <tbody>
    <tr ng-repeat="rows in data.rows">
      <td ng-repeat="days in row.days ng-click="select()">{{ days.date }}</td>
    </tr>
  </tbody>
</table>

But, we want to inject a button within the table cell to accommodate proper accessibility.

<table>
  <tbody>
    <tr ng-repeat="rows in data.rows">
      <td ng-repeat="days in row.days>
        <button type="button" ng-click="select()">{{ days.date }}</button>
      </td>
    </tr>
  </tbody>
</table>

For very, very basic accessibility. This will allow the buttons within the table to be accessed by keyboard users using the tab key. By default, pressing enter on a button will also trigger its click event. However, this will only allow tabbing through the controls within the calendar which can be cumbersome. So, while it becomes accessible, it’s at a bare-minimum. Incorporating the ability to navigate through the calendar with the keyboard’s arrow keys provides much better, and possibly expected, accessibility.

Within the directive, to accommodate arrow key navigation within an HTML table, we can attach a keydown/keypress handler to intercept arrow key presses. Using some simple jQuery, we calculate the current position and “next” position of the HTML thead or tbody and apply the appropriate focus.

$element.bind("keydown keypress", function (event) {
    var keyCode = event.which || event.keyCode,
        arrowKeysArr = [37, 38, 39, 40];
    if (!arrowKeysArr.find(function (val) { return val == keyCode })) { return; }
    var
        arrowkeys = { left: 37, up: 38, right: 39, down: 40 },
        moveX = 0,
        moveY = 0,
        target = event.target,
        tagName = target.tagName.toLowerCase(),
        $target = angular.element(event.target),
        $headRows = $element.find('table').find('thead').children().eq(0),
        $bodyRows = $element.find('table').find('tbody').children(),
        isInBody = $target.parents('tbody').length > 0 && $target.parent()[0].tagName.toLowerCase() === 'td',
        $parent = tagName === 'i' || tagName === 'button' ? $target.parent() : $target,
        $tableCell = null;

    // Are we in a cell?
    if (tagName === 'td' || tagName === 'th') {
        $tableCell = $target;
    } else if (tagName !== 'table') {
        $tableCell = $target.parent();
    }

    if ($tableCell) {
        var $row = $tableCell.closest('tr');
        var $rows = isInBody ? $bodyRows : $headRows;
        var rowIndex = moveY = $rows.index($row);
        var colIndex = moveX = $row.children().index($tableCell);
        var colCount = $row.children().length;
        var hasRowAbove = rowIndex > 0;
        var hasRowBelow = rowIndex < ($rows.length - 1);

        switch (keyCode) {
            case arrowkeys.left:
                if (colIndex > 0) {
                    moveX = colIndex - 1;
                } else if (hasRowAbove) {
                    moveX = colCount - 1;
                    moveY = rowIndex - 1;
                }
                break;
            case arrowkeys.right:
                if (colIndex < (colCount - 1)) {
                    moveX = colIndex + 1;
                } else if (hasRowBelow) {
                    moveX = 0;
                    moveY = rowIndex + 1;
                }
                break;
            case arrowkeys.up:
                if (hasRowAbove) {
                    moveY = rowIndex - 1;
                }
                break;
            case arrowkeys.down:
                if (hasRowBelow) {
                    moveY = rowIndex + 1;
                }
                break;
        }

        var $moveToRow = $rows.eq(moveY);
        var $moveToCol = $moveToRow.children().eq(moveX);
        var $focusElement = isInBody ? $moveToCol.children('button') : $moveToCol;
        $focusElement[0].focus();
        event.preventDefault();
    }

    // If enter key is pressed
    if (keyCode === 13) {
        scope.$apply(function () {
            // Evaluate the expression
            //scope.$eval(attrs.dlEnterKey);
        });

        event.preventDefault();
    }
});

The above code could, realistically, be applied to any HTML table to provide navigation of the table too.

Of course, there is more to accessibility than simple keyboard navigation. To make any HTML control fully accessible, it also needs to have the appropriate aria/role attributes applied so that screen readers work properly. Having keyboard accessibility, where previously there was none, is a first good step.

2 thoughts on “Working with Web Accessibility”

    1. Dropdowns are pain all-around. NVDA/Narrator will sometimes announce then as “sub menu,” which is wrong. ui-select2 is problematic in how it focuses on the blank text on initial load rather than the first item. That causes a compliance issues, especially with NVDA/Narrator, since they both will just say “Blank.”

      Fortunately, most of the older AngularJS (minus ui-select2) Bootstrap directives will let you swap out their templates to at least make them more accessibility compliant. But, it’s still a pain. At some point, I may just give up on ui-select2 and use my own multiselect dropdown since I have full control over its behavior.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.