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

Commit 5fbabe7

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 bc71d0b commit 5fbabe7

File tree

4 files changed

+130
-9
lines changed

4 files changed

+130
-9
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: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,7 @@ function SelectMenuDirective($parse, $mdUtil, $mdConstant, $mdTheming) {
741741
return self.options;
742742
}, function() {
743743
self.ngModel.$render();
744+
updateOptionSetSizeAndPosition();
744745
});
745746

746747
/**
@@ -1067,9 +1068,32 @@ function SelectMenuDirective($parse, $mdUtil, $mdConstant, $mdTheming) {
10671068
}
10681069
};
10691070

1071+
/**
1072+
* If the options include md-optgroups, then we need to apply aria-setsize and aria-posinset
1073+
* to help screen readers understand the indexes. When md-optgroups are not used, we save on
1074+
* perf and extra attributes by not applying these attributes as they are not needed by screen
1075+
* readers.
1076+
*/
1077+
function updateOptionSetSizeAndPosition() {
1078+
var i, options;
1079+
var hasOptGroup = $element.find('md-optgroup');
1080+
if (!hasOptGroup.length) {
1081+
return;
1082+
}
1083+
1084+
options = $element.find('md-option');
1085+
1086+
for (i = 0; i < options.length; i++) {
1087+
options[i].setAttribute('aria-setsize', options.length);
1088+
options[i].setAttribute('aria-posinset', i + 1);
1089+
}
1090+
}
1091+
10701092
function renderMultiple() {
10711093
var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || [];
1072-
if (!angular.isArray(newSelectedValues)) return;
1094+
if (!angular.isArray(newSelectedValues)) {
1095+
return;
1096+
}
10731097

10741098
var oldSelected = Object.keys(self.selected);
10751099

@@ -1371,6 +1395,7 @@ function OptgroupDirective() {
13711395
if (!hasSelectHeader()) {
13721396
setupLabelElement();
13731397
}
1398+
element.attr('role', 'group');
13741399

13751400
function hasSelectHeader() {
13761401
return element.parent().find('md-select-header').length;
@@ -1387,6 +1412,7 @@ function OptgroupDirective() {
13871412
if (attrs.label) {
13881413
labelElement.text(attrs.label);
13891414
}
1415+
element.attr('aria-label', labelElement.text());
13901416
}
13911417
}
13921418
}
@@ -1590,7 +1616,7 @@ function SelectProvider($$interimElementProvider) {
15901616
/**
15911617
* @param {Element|HTMLElement|null=} previousNode
15921618
* @param {Element|HTMLElement} node
1593-
* @param {SelectMenuController|Function|Object=} menuController SelectMenuController instance
1619+
* @param {SelectMenuController|Function|object=} menuController SelectMenuController instance
15941620
*/
15951621
function focusOptionNode(previousNode, node, menuController) {
15961622
var listboxContentNode = opts.contentEl[0];

src/components/select/select.spec.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,92 @@ describe('<md-select>', function() {
14461446
clickOption(el, 2);
14471447
expect(options.eq(2).attr('aria-selected')).toBe('true');
14481448
});
1449+
1450+
it('applies label element\'s text to optgroup\'s aria-label', function() {
1451+
$rootScope.val = [1];
1452+
var select = $compile(
1453+
'<md-input-container>' +
1454+
' <label>Label</label>' +
1455+
' <md-select ng-model="val" placeholder="Hello World">' +
1456+
' <md-optgroup>' +
1457+
' <label>stuff</label>' +
1458+
' <md-option value="1">One</md-option>' +
1459+
' <md-option value="2">Two</md-option>' +
1460+
' <md-option value="3">Three</md-option>' +
1461+
' </md-optgroup>' +
1462+
' </md-select>' +
1463+
'</md-input-container>')($rootScope);
1464+
1465+
var optgroups = select.find('md-optgroup');
1466+
expect(optgroups[0].getAttribute('aria-label')).toBe('stuff');
1467+
});
1468+
1469+
it('applies optgroup\'s label as aria-label', function() {
1470+
$rootScope.val = [1];
1471+
var select = $compile(
1472+
'<md-input-container>' +
1473+
' <label>Label</label>' +
1474+
' <md-select ng-model="val" placeholder="Hello World">' +
1475+
' <md-optgroup label="stuff">' +
1476+
' <md-option value="1">One</md-option>' +
1477+
' <md-option value="2">Two</md-option>' +
1478+
' <md-option value="3">Three</md-option>' +
1479+
' </md-optgroup>' +
1480+
' </md-select>' +
1481+
'</md-input-container>')($rootScope);
1482+
1483+
var optgroups = select.find('md-optgroup');
1484+
expect(optgroups[0].getAttribute('aria-label')).toBe('stuff');
1485+
});
1486+
1487+
it('applies setsize and posinset when optgroups are used', function() {
1488+
$rootScope.val = [1];
1489+
var select = $compile(
1490+
'<md-input-container>' +
1491+
' <label>Label</label>' +
1492+
' <md-select ng-model="val" placeholder="Hello World">' +
1493+
' <md-optgroup label="stuff">' +
1494+
' <md-option value="1">One</md-option>' +
1495+
' <md-option value="2">Two</md-option>' +
1496+
' <md-option value="3">Three</md-option>' +
1497+
' </md-optgroup>' +
1498+
' </md-select>' +
1499+
'</md-input-container>')($rootScope);
1500+
$rootScope.$digest();
1501+
1502+
var options = select.find('md-option');
1503+
expect(options[0].getAttribute('aria-setsize')).toBe('3');
1504+
expect(options[0].getAttribute('aria-posinset')).toBe('1');
1505+
});
1506+
1507+
it('applies setsize and posinset when optgroups are used with multiple', function() {
1508+
$rootScope.val = [1];
1509+
var select = $compile(
1510+
'<md-input-container>' +
1511+
' <label>Label</label>' +
1512+
' <md-select multiple ng-model="val" placeholder="Hello World">' +
1513+
' <md-optgroup label="stuff">' +
1514+
' <md-option value="1">One</md-option>' +
1515+
' <md-option value="2">Two</md-option>' +
1516+
' <md-option value="3">Three</md-option>' +
1517+
' </md-optgroup>' +
1518+
' </md-select>' +
1519+
'</md-input-container>')($rootScope);
1520+
$rootScope.$digest();
1521+
1522+
var options = select.find('md-option');
1523+
expect(options[0].getAttribute('aria-setsize')).toBe('3');
1524+
expect(options[0].getAttribute('aria-posinset')).toBe('1');
1525+
});
1526+
1527+
it('does not apply setsize and posinset when optgroups are not used', function() {
1528+
var select = setupSelect('ng-model="$root.model"', [1, 2, 3]);
1529+
$rootScope.$digest();
1530+
1531+
var options = select.find('md-option');
1532+
expect(options[0].getAttribute('aria-setsize')).toBe(null);
1533+
expect(options[0].getAttribute('aria-posinset')).toBe(null);
1534+
});
14491535
});
14501536

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

0 commit comments

Comments
 (0)