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

fix(select): optgroups are not visible to screen readers #11771

Merged
merged 1 commit into from
May 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/components/select/demoOptionGroups/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,26 @@ <h1 class="md-title">Pick your pizza below</h1>
<md-input-container style="margin-right: 10px;">
<label>Size</label>
<md-select ng-model="size">
<md-option ng-repeat="size in sizes" value="{{size}}">{{size}}</md-option>
<md-optgroup label="No Surcharge">
<md-option ng-repeat="size in sizes | filter: {surcharge: 'none'}"
value="{{size.name}}">{{size.name}}</md-option>
</md-optgroup>
<md-optgroup label="Additional Surcharge">
<md-option ng-repeat="size in sizes | filter: {surcharge: 'extra'}"
value="{{size.name}}">{{size.name}}</md-option>
</md-optgroup>
</md-select>
</md-input-container>
<md-input-container>
<label>Toppings</label>
<md-select ng-model="selectedToppings" multiple>
<md-optgroup label="Meats">
<md-option ng-value="topping.name" ng-repeat="topping in toppings | filter: {category: 'meat' }">{{topping.name}}</md-option>
<md-option ng-value="topping.name" ng-repeat="topping in toppings | filter: {category: 'meat'}">
{{topping.name}}</md-option>
</md-optgroup>
<md-optgroup label="Veggies">
<md-option ng-value="topping.name" ng-repeat="topping in toppings | filter: {category: 'veg' }">{{topping.name}}</md-option>
<md-option ng-value="topping.name" ng-repeat="topping in toppings | filter: {category: 'veg'}">
{{topping.name}}</md-option>
</md-optgroup>
</md-select>
</md-input-container>
Expand Down
8 changes: 4 additions & 4 deletions src/components/select/demoOptionGroups/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ angular
.module('selectDemoOptGroups', ['ngMaterial'])
.controller('SelectOptGroupController', function($scope) {
$scope.sizes = [
"small (12-inch)",
"medium (14-inch)",
"large (16-inch)",
"insane (42-inch)"
{ surcharge: 'none', name: "small (12-inch)" },
{ surcharge: 'none', name: "medium (14-inch)" },
{ surcharge: 'extra', name: "large (16-inch)" },
{ surcharge: 'extra', name: "insane (42-inch)" }
];
$scope.toppings = [
{ category: 'meat', name: 'Pepperoni' },
Expand Down
30 changes: 28 additions & 2 deletions src/components/select/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@ function SelectMenuDirective($parse, $mdUtil, $mdConstant, $mdTheming) {
return self.options;
}, function() {
self.ngModel.$render();
updateOptionSetSizeAndPosition();
});

/**
Expand Down Expand Up @@ -1067,9 +1068,32 @@ function SelectMenuDirective($parse, $mdUtil, $mdConstant, $mdTheming) {
}
};

/**
* If the options include md-optgroups, then we need to apply aria-setsize and aria-posinset
* to help screen readers understand the indexes. When md-optgroups are not used, we save on
* perf and extra attributes by not applying these attributes as they are not needed by screen
* readers.
*/
function updateOptionSetSizeAndPosition() {
var i, options;
var hasOptGroup = $element.find('md-optgroup');
if (!hasOptGroup.length) {
return;
}

options = $element.find('md-option');

for (i = 0; i < options.length; i++) {
options[i].setAttribute('aria-setsize', options.length);
options[i].setAttribute('aria-posinset', i + 1);
}
}

function renderMultiple() {
var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || [];
if (!angular.isArray(newSelectedValues)) return;
if (!angular.isArray(newSelectedValues)) {
return;
}

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

Expand Down Expand Up @@ -1371,6 +1395,7 @@ function OptgroupDirective() {
if (!hasSelectHeader()) {
setupLabelElement();
}
element.attr('role', 'group');

function hasSelectHeader() {
return element.parent().find('md-select-header').length;
Expand All @@ -1387,6 +1412,7 @@ function OptgroupDirective() {
if (attrs.label) {
labelElement.text(attrs.label);
}
element.attr('aria-label', labelElement.text());
}
}
}
Expand Down Expand Up @@ -1590,7 +1616,7 @@ function SelectProvider($$interimElementProvider) {
/**
* @param {Element|HTMLElement|null=} previousNode
* @param {Element|HTMLElement} node
* @param {SelectMenuController|Function|Object=} menuController SelectMenuController instance
* @param {SelectMenuController|Function|object=} menuController SelectMenuController instance
*/
function focusOptionNode(previousNode, node, menuController) {
var listboxContentNode = opts.contentEl[0];
Expand Down
86 changes: 86 additions & 0 deletions src/components/select/select.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,92 @@ describe('<md-select>', function() {
clickOption(el, 2);
expect(options.eq(2).attr('aria-selected')).toBe('true');
});

it('applies label element\'s text to optgroup\'s aria-label', function() {
$rootScope.val = [1];
var select = $compile(
'<md-input-container>' +
' <label>Label</label>' +
' <md-select ng-model="val" placeholder="Hello World">' +
' <md-optgroup>' +
' <label>stuff</label>' +
' <md-option value="1">One</md-option>' +
' <md-option value="2">Two</md-option>' +
' <md-option value="3">Three</md-option>' +
' </md-optgroup>' +
' </md-select>' +
'</md-input-container>')($rootScope);

var optgroups = select.find('md-optgroup');
expect(optgroups[0].getAttribute('aria-label')).toBe('stuff');
});

it('applies optgroup\'s label as aria-label', function() {
$rootScope.val = [1];
var select = $compile(
'<md-input-container>' +
' <label>Label</label>' +
' <md-select ng-model="val" placeholder="Hello World">' +
' <md-optgroup label="stuff">' +
' <md-option value="1">One</md-option>' +
' <md-option value="2">Two</md-option>' +
' <md-option value="3">Three</md-option>' +
' </md-optgroup>' +
' </md-select>' +
'</md-input-container>')($rootScope);

var optgroups = select.find('md-optgroup');
expect(optgroups[0].getAttribute('aria-label')).toBe('stuff');
});

it('applies setsize and posinset when optgroups are used', function() {
$rootScope.val = [1];
var select = $compile(
'<md-input-container>' +
' <label>Label</label>' +
' <md-select ng-model="val" placeholder="Hello World">' +
' <md-optgroup label="stuff">' +
' <md-option value="1">One</md-option>' +
' <md-option value="2">Two</md-option>' +
' <md-option value="3">Three</md-option>' +
' </md-optgroup>' +
' </md-select>' +
'</md-input-container>')($rootScope);
$rootScope.$digest();

var options = select.find('md-option');
expect(options[0].getAttribute('aria-setsize')).toBe('3');
expect(options[0].getAttribute('aria-posinset')).toBe('1');
});

it('applies setsize and posinset when optgroups are used with multiple', function() {
$rootScope.val = [1];
var select = $compile(
'<md-input-container>' +
' <label>Label</label>' +
' <md-select multiple ng-model="val" placeholder="Hello World">' +
' <md-optgroup label="stuff">' +
' <md-option value="1">One</md-option>' +
' <md-option value="2">Two</md-option>' +
' <md-option value="3">Three</md-option>' +
' </md-optgroup>' +
' </md-select>' +
'</md-input-container>')($rootScope);
$rootScope.$digest();

var options = select.find('md-option');
expect(options[0].getAttribute('aria-setsize')).toBe('3');
expect(options[0].getAttribute('aria-posinset')).toBe('1');
});

it('does not apply setsize and posinset when optgroups are not used', function() {
var select = setupSelect('ng-model="$root.model"', [1, 2, 3]);
$rootScope.$digest();

var options = select.find('md-option');
expect(options[0].getAttribute('aria-setsize')).toBe(null);
expect(options[0].getAttribute('aria-posinset')).toBe(null);
});
});

describe('keyboard controls', function() {
Expand Down