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

Commit 4c76a85

Browse files
committed
feat(typeahead): add aria-owns & aria-activedescendant roles
1 parent 82df4fb commit 4c76a85

File tree

3 files changed

+35
-9
lines changed

3 files changed

+35
-9
lines changed

src/typeahead/test/typeahead.spec.js

+8
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,23 @@ describe('typeahead tests', function () {
131131
it('should open and close typeahead based on matches', function () {
132132
var element = prepareInputEl('<div><input ng-model="result" typeahead="item for item in source | filter:$viewValue"></div>');
133133
var inputEl = findInput(element);
134+
var ownsId = inputEl.attr('aria-owns');
135+
134136
expect(inputEl.attr('aria-expanded')).toBe('false');
137+
expect(inputEl.attr('aria-activedescendant')).toBeUndefined();
135138

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

140147
changeInputValueTo(element, '');
141148
expect(element).toBeClosed();
142149
expect(inputEl.attr('aria-expanded')).toBe('false');
150+
expect(inputEl.attr('aria-activedescendant')).toBeUndefined();
143151
});
144152

145153
it('should not open typeahead if input value smaller than a defined threshold', function () {

src/typeahead/typeahead.js

+26-8
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,25 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
6969

7070
var hasFocus;
7171

72+
//create a child scope for the typeahead directive so we are not polluting original scope
73+
//with typeahead-specific data (matches, query etc.)
74+
var scope = originalScope.$new();
75+
originalScope.$on('$destroy', function(){
76+
scope.$destroy();
77+
});
78+
7279
// WAI-ARIA
80+
var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
7381
element.attr({
7482
'aria-autocomplete': 'list',
75-
'aria-expanded': false
83+
'aria-expanded': false,
84+
'aria-owns': popupId
7685
});
7786

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

92-
//create a child scope for the typeahead directive so we are not polluting original scope
93-
//with typeahead-specific data (matches, query etc.)
94-
var scope = originalScope.$new();
95-
originalScope.$on('$destroy', function(){
96-
scope.$destroy();
97-
});
98-
99102
var resetMatches = function() {
100103
scope.matches = [];
101104
scope.activeIdx = -1;
102105
element.attr('aria-expanded', false);
103106
};
104107

108+
var getMatchId = function(index) {
109+
return popupId + '-option-' + index;
110+
};
111+
112+
// Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
113+
// This attribute is added or removed automatically when the `activeIdx` changes.
114+
scope.$watch('activeIdx', function(index) {
115+
if (index < 0) {
116+
element.removeAttr('aria-activedescendant');
117+
} else {
118+
element.attr('aria-activedescendant', getMatchId(index));
119+
}
120+
});
121+
105122
var getMatchesAsync = function(inputValue) {
106123

107124
var locals = {$viewValue: inputValue};
@@ -121,6 +138,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
121138
for(var i=0; i<matches.length; i++) {
122139
locals[parserResult.itemName] = matches[i];
123140
scope.matches.push({
141+
id: getMatchId(i),
124142
label: parserResult.viewMapper(scope, locals),
125143
model: matches[i]
126144
});
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<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()}}">
2-
<li ng-repeat="match in matches track by $index" ng-class="{active: isActive($index) }" ng-mouseenter="selectActive($index)" ng-click="selectMatch($index)" role="option">
2+
<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}}">
33
<div typeahead-match index="$index" match="match" query="query" template-url="templateUrl"></div>
44
</li>
55
</ul>

0 commit comments

Comments
 (0)