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

Commit

Permalink
fix(select): optgroups are not visible to screen readers
Browse files Browse the repository at this point in the history
add single selection optgroup demo
add tests for optgroup `aria-label`
add tests for optgroup options' `aria-setsize` and `aria-posinset`

Fixes #11240
  • Loading branch information
Splaktar committed May 15, 2020
1 parent d1305be commit 2cc614f
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 9 deletions.
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

0 comments on commit 2cc614f

Please sign in to comment.