AngularJS と NVDA

Share

<htmlday> 2013 ということで AngularJS で遊ぼう という勉強会に参加しました。

AngularJS は Web アプリケーション開発のフレームワークということで、簡潔に書けて保守性が高いのが特長、らしいです。

しかし、生産性の高い開発手法という話を聞くと「それってアクセシビリティは大丈夫なの?」とつい思ってしまいます。
そこで、angularjs.org のトップページのビデオで紹介されていた TODO アプリケーションのサンプルをスクリーンリーダー NVDA 日本語版 と Firefox の組み合わせで確認しました。

書かれたマークアップは内部で標準的なHTMLに変換されているので、フォーカス移動などはちゃんとできるのですが、チェックボックスにフォーカスしたときに、その右側にある項目名を読み上げないので、何の操作をするチェックボックスなのかわかりません。

ふと aria-labelledby でやればいいんじゃない? と思ったので、下記のように直したところ、うまくチェックボックスの項目名を読み上げるようになりました。

最初は id をつけるために todos モデルに通し番号のプロパティを追加したのですが、後で $index という特殊プロパティがあることを教わったので、todo.js 側のコントローラーとモデルは書き換えないで実装できました。アクセシビリティのための処理が View だけで完結するのは素晴らしい。AngularJS なかなかよさそうなのでもうちょっと勉強してみたいと思います。

<htmlday>とは、日本全国でWeb制作者/開発者向けのイベントを同日に開催することで、日本のWebを一層盛り上げようという「お祭り」だったそうで、私も貢献できればと思いブログを書きました。
ちなみに広島市中心部は「とうかさん」というお祭りの季節です。。

以下のソースでは bootstrap.cssUnderscore.js を使っています:

<!doctype html>
<html ng-app>
  <head>
    <script src="angular.js"></script>
    <script src="underscore.js"></script>
    <script src="todo.js"></script>
    <link rel="stylesheet" href="bootstrap/css/bootstrap.css">
    <link rel="stylesheet" href="todo.css">
  </head>
  <body>
    <h2>Todo</h2>
    <div ng-controller="TodoCtrl">
      <span>{{remaining()}} of {{todos.length}} remaining</span>
      [ <a href="" ng-click="archive()">archive</a> ]
      <ul>
        <li ng-repeat="todo in todos">
          <input aria-labelledby="label{{$index}}" type="checkbox" ng-model="todo.done">
          <span id="label{{$index}}">{{todo.text}}</span>
        </li>
      </ul>
      <form>
        <input type="text" ng-model="todoText"  size="30"
               placeholder="add new todo here">
        <button ng-click="addTodo()"><i></i>Add</button>
      </form>

      <button ng-click="clearCompleted()"><i></i>Clear Completed</button>
    </div>
  </body>
</html>
// todo.js
function TodoCtrl($scope) {
    $scope.todos = [
	{text:'learn angular', done:true},
	{text:'build an angular app', done:false}];
    $scope.addTodo = function() {
	$scope.todos.push({text:$scope.todoText, done:false});
	$scope.todoText = '';
    };
    $scope.remaining = function() {
	var count = 0;
	angular.forEach($scope.todos, function(todo) {
	    count += todo.done ? 0 : 1;
	});
	return count;
    };
    $scope.archive = function() {
	var oldTodos = $scope.todos;
	$scope.todos = [];
	angular.forEach(oldTodos, function(todo) {
	    if (!todo.done) $scope.todos.push(todo);
	});
    };
    $scope.clearCompleted = function() {
	$scope.todos = _.filter($scope.todos, function(todo) {
	    return !todo.done;
	})
    };
}