Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit d07d693

Browse files
committed
fix(select): optgroups are not visible to screen readers
add single selection optgroup demo add tests for optgroup `aria-label` add tests for optgroup options' `aria-setsize` and `aria-posinset` Fixes #11240
1 parent 4694e08 commit d07d693

File tree

4 files changed

+129
-8
lines changed

4 files changed

+129
-8
lines changed

src/components/select/demoOptionGroups/index.html

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,26 @@ <h1 class="md-title">Pick your pizza below</h1>
55
<md-input-container style="margin-right: 10px;">
66
<label>Size</label>
77
<md-select ng-model="size">
8-
<md-option ng-repeat="size in sizes" value="{{size}}">{{size}}</md-option>
8+
<md-optgroup label="No Surcharge">
9+
<md-option ng-repeat="size in sizes | filter: {surcharge: 'none'}"
10+
value="{{size.name}}">{{size.name}}</md-option>
11+
</md-optgroup>
12+
<md-optgroup label="Additional Surcharge">
13+
<md-option ng-repeat="size in sizes | filter: {surcharge: 'extra'}"
14+
value="{{size.name}}">{{size.name}}</md-option>
15+
</md-optgroup>
916
</md-select>
1017
</md-input-container>
1118
<md-input-container>
1219
<label>Toppings</label>
1320
<md-select ng-model="selectedToppings" multiple>
1421
<md-optgroup label="Meats">
15-
<md-option ng-value="topping.name" ng-repeat="topping in toppings | filter: {category: 'meat' }">{{topping.name}}</md-option>
22+
<md-option ng-value="topping.name" ng-repeat="topping in toppings | filter: {category: 'meat'}">
23+
{{topping.name}}</md-option>
1624
</md-optgroup>
1725
<md-optgroup label="Veggies">
18-
<md-option ng-value="topping.name" ng-repeat="topping in toppings | filter: {category: 'veg' }">{{topping.name}}</md-option>
26+
<md-option ng-value="topping.name" ng-repeat="topping in toppings | filter: {category: 'veg'}">
27+
{{topping.name}}</md-option>
1928
</md-optgroup>
2029
</md-select>
2130
</md-input-container>

src/components/select/demoOptionGroups/script.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ angular
22
.module('selectDemoOptGroups', ['ngMaterial'])
33
.controller('SelectOptGroupController', function($scope) {
44
$scope.sizes = [
5-
"small (12-inch)",
6-
"medium (14-inch)",
7-
"large (16-inch)",
8-
"insane (42-inch)"
5+
{ surcharge: 'none', name: "small (12-inch)" },
6+
{ surcharge: 'none', name: "medium (14-inch)" },
7+
{ surcharge: 'extra', name: "large (16-inch)" },
8+
{ surcharge: 'extra', name: "insane (42-inch)" }
99
];
1010
$scope.toppings = [
1111
{ category: 'meat', name: 'Pepperoni' },

src/components/select/select.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,7 @@ function SelectMenuDirective($parse, $mdUtil, $mdConstant, $mdTheming) {
744744
return self.options;
745745
}, function() {
746746
self.ngModel.$render();
747+
updateOptionSetSizeAndPosition();
747748
});
748749

749750
/**
@@ -1016,9 +1017,32 @@ function SelectMenuDirective($parse, $mdUtil, $mdConstant, $mdTheming) {
10161017
}
10171018
};
10181019

1020+
/**
1021+
* If the options include md-optgroups, then we need to apply aria-setsize and aria-posinset
1022+
* to help screen readers understand the indexes. When md-optgroups are not used, we save on
1023+
* perf and extra attributes by not applying these attributes as they are not needed by screen
1024+
* readers.
1025+
*/
1026+
function updateOptionSetSizeAndPosition() {
1027+
var i, options;
1028+
var hasOptGroup = $element.find('md-optgroup');
1029+
if (!hasOptGroup.length) {
1030+
return;
1031+
}
1032+
1033+
options = $element.find('md-option');
1034+
1035+
for (i = 0; i < options.length; i++) {
1036+
options[i].setAttribute('aria-setsize', options.length);
1037+
options[i].setAttribute('aria-posinset', i + 1);
1038+
}
1039+
}
1040+
10191041
function renderMultiple() {
10201042
var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || [];
1021-
if (!angular.isArray(newSelectedValues)) return;
1043+
if (!angular.isArray(newSelectedValues)) {
1044+
return;
1045+
}
10221046

10231047
var oldSelected = Object.keys(self.selected);
10241048

@@ -1320,6 +1344,7 @@ function OptgroupDirective() {
13201344
if (!hasSelectHeader()) {
13211345
setupLabelElement();
13221346
}
1347+
element.attr('role', 'group');
13231348

13241349
function hasSelectHeader() {
13251350
return element.parent().find('md-select-header').length;
@@ -1336,6 +1361,7 @@ function OptgroupDirective() {
13361361
if (attrs.label) {
13371362
labelElement.text(attrs.label);
13381363
}
1364+
element.attr('aria-label', labelElement.text());
13391365
}
13401366
}
13411367
}

src/components/select/select.spec.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,6 +1375,92 @@ describe('<md-select>', function() {
13751375
clickOption(el, 2);
13761376
expect(options.eq(2).attr('aria-selected')).toBe('true');
13771377
});
1378+
1379+
it('applies label element\'s text to optgroup\'s aria-label', function() {
1380+
$rootScope.val = [1];
1381+
var select = $compile(
1382+
'<md-input-container>' +
1383+
' <label>Label</label>' +
1384+
' <md-select ng-model="val" placeholder="Hello World">' +
1385+
' <md-optgroup>' +
1386+
' <label>stuff</label>' +
1387+
' <md-option value="1">One</md-option>' +
1388+
' <md-option value="2">Two</md-option>' +
1389+
' <md-option value="3">Three</md-option>' +
1390+
' </md-optgroup>' +
1391+
' </md-select>' +
1392+
'</md-input-container>')($rootScope);
1393+
1394+
var optgroups = select.find('md-optgroup');
1395+
expect(optgroups[0].getAttribute('aria-label')).toBe('stuff');
1396+
});
1397+
1398+
it('applies optgroup\'s label as aria-label', function() {
1399+
$rootScope.val = [1];
1400+
var select = $compile(
1401+
'<md-input-container>' +
1402+
' <label>Label</label>' +
1403+
' <md-select ng-model="val" placeholder="Hello World">' +
1404+
' <md-optgroup label="stuff">' +
1405+
' <md-option value="1">One</md-option>' +
1406+
' <md-option value="2">Two</md-option>' +
1407+
' <md-option value="3">Three</md-option>' +
1408+
' </md-optgroup>' +
1409+
' </md-select>' +
1410+
'</md-input-container>')($rootScope);
1411+
1412+
var optgroups = select.find('md-optgroup');
1413+
expect(optgroups[0].getAttribute('aria-label')).toBe('stuff');
1414+
});
1415+
1416+
it('applies setsize and posinset when optgroups are used', function() {
1417+
$rootScope.val = [1];
1418+
var select = $compile(
1419+
'<md-input-container>' +
1420+
' <label>Label</label>' +
1421+
' <md-select ng-model="val" placeholder="Hello World">' +
1422+
' <md-optgroup label="stuff">' +
1423+
' <md-option value="1">One</md-option>' +
1424+
' <md-option value="2">Two</md-option>' +
1425+
' <md-option value="3">Three</md-option>' +
1426+
' </md-optgroup>' +
1427+
' </md-select>' +
1428+
'</md-input-container>')($rootScope);
1429+
$rootScope.$digest();
1430+
1431+
var options = select.find('md-option');
1432+
expect(options[0].getAttribute('aria-setsize')).toBe('3');
1433+
expect(options[0].getAttribute('aria-posinset')).toBe('1');
1434+
});
1435+
1436+
it('applies setsize and posinset when optgroups are used with multiple', function() {
1437+
$rootScope.val = [1];
1438+
var select = $compile(
1439+
'<md-input-container>' +
1440+
' <label>Label</label>' +
1441+
' <md-select multiple ng-model="val" placeholder="Hello World">' +
1442+
' <md-optgroup label="stuff">' +
1443+
' <md-option value="1">One</md-option>' +
1444+
' <md-option value="2">Two</md-option>' +
1445+
' <md-option value="3">Three</md-option>' +
1446+
' </md-optgroup>' +
1447+
' </md-select>' +
1448+
'</md-input-container>')($rootScope);
1449+
$rootScope.$digest();
1450+
1451+
var options = select.find('md-option');
1452+
expect(options[0].getAttribute('aria-setsize')).toBe('3');
1453+
expect(options[0].getAttribute('aria-posinset')).toBe('1');
1454+
});
1455+
1456+
it('does not apply setsize and posinset when optgroups are not used', function() {
1457+
var select = setupSelect('ng-model="$root.model"', [1, 2, 3]);
1458+
$rootScope.$digest();
1459+
1460+
var options = select.find('md-option');
1461+
expect(options[0].getAttribute('aria-setsize')).toBe(null);
1462+
expect(options[0].getAttribute('aria-posinset')).toBe(null);
1463+
});
13781464
});
13791465

13801466
describe('keyboard controls', function() {

0 commit comments

Comments
 (0)