Skip to content
This repository has been archived by the owner on May 29, 2019. It is now read-only.

Commit

Permalink
feat(typeahead): add aria-owns & aria-activedescendant roles
Browse files Browse the repository at this point in the history
  • Loading branch information
bekos committed Apr 11, 2014
1 parent 82df4fb commit 4c76a85
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 9 deletions.
8 changes: 8 additions & 0 deletions src/typeahead/test/typeahead.spec.js
Expand Up @@ -131,15 +131,23 @@ describe('typeahead tests', function () {
it('should open and close typeahead based on matches', function () {
var element = prepareInputEl('<div><input ng-model="result" typeahead="item for item in source | filter:$viewValue"></div>');
var inputEl = findInput(element);
var ownsId = inputEl.attr('aria-owns');

expect(inputEl.attr('aria-expanded')).toBe('false');
expect(inputEl.attr('aria-activedescendant')).toBeUndefined();

changeInputValueTo(element, 'ba');
expect(element).toBeOpenWithActive(2, 0);
expect(findDropDown(element).attr('id')).toBe(ownsId);
expect(inputEl.attr('aria-expanded')).toBe('true');
var activeOptionId = ownsId + '-option-0';
expect(inputEl.attr('aria-activedescendant')).toBe(activeOptionId);
expect(findDropDown(element).find('li.active').attr('id')).toBe(activeOptionId);

changeInputValueTo(element, '');
expect(element).toBeClosed();
expect(inputEl.attr('aria-expanded')).toBe('false');
expect(inputEl.attr('aria-activedescendant')).toBeUndefined();
});

it('should not open typeahead if input value smaller than a defined threshold', function () {
Expand Down
34 changes: 26 additions & 8 deletions src/typeahead/typeahead.js
Expand Up @@ -69,15 +69,25 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap

var hasFocus;

//create a child scope for the typeahead directive so we are not polluting original scope
//with typeahead-specific data (matches, query etc.)
var scope = originalScope.$new();
originalScope.$on('$destroy', function(){
scope.$destroy();
});

// WAI-ARIA
var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
element.attr({
'aria-autocomplete': 'list',
'aria-expanded': false
'aria-expanded': false,
'aria-owns': popupId
});

//pop-up element used to display matches
var popUpEl = angular.element('<div typeahead-popup></div>');
popUpEl.attr({
id: popupId,
matches: 'matches',
active: 'activeIdx',
select: 'select(activeIdx)',
Expand All @@ -89,19 +99,26 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
}

//create a child scope for the typeahead directive so we are not polluting original scope
//with typeahead-specific data (matches, query etc.)
var scope = originalScope.$new();
originalScope.$on('$destroy', function(){
scope.$destroy();
});

var resetMatches = function() {
scope.matches = [];
scope.activeIdx = -1;
element.attr('aria-expanded', false);
};

var getMatchId = function(index) {
return popupId + '-option-' + index;
};

// Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
// This attribute is added or removed automatically when the `activeIdx` changes.
scope.$watch('activeIdx', function(index) {
if (index < 0) {
element.removeAttr('aria-activedescendant');
} else {
element.attr('aria-activedescendant', getMatchId(index));
}
});

var getMatchesAsync = function(inputValue) {

var locals = {$viewValue: inputValue};
Expand All @@ -121,6 +138,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
for(var i=0; i<matches.length; i++) {
locals[parserResult.itemName] = matches[i];
scope.matches.push({
id: getMatchId(i),
label: parserResult.viewMapper(scope, locals),
model: matches[i]
});
Expand Down
2 changes: 1 addition & 1 deletion template/typeahead/typeahead-popup.html
@@ -1,5 +1,5 @@
<ul class="dropdown-menu" ng-if="isOpen()" ng-style="{top: position.top+'px', left: position.left+'px'}" style="display: block;" role="listbox" aria-hidden="{{!isOpen()}}">
<li ng-repeat="match in matches track by $index" ng-class="{active: isActive($index) }" ng-mouseenter="selectActive($index)" ng-click="selectMatch($index)" role="option">
<li ng-repeat="match in matches track by $index" ng-class="{active: isActive($index) }" ng-mouseenter="selectActive($index)" ng-click="selectMatch($index)" role="option" id="{{match.id}}">
<div typeahead-match index="$index" match="match" query="query" template-url="templateUrl"></div>
</li>
</ul>

0 comments on commit 4c76a85

Please sign in to comment.