From 2cc614fa6beae41c5722eab2427e5b77255d4d55 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 22 Jun 2019 20:08:22 -0400 Subject: [PATCH] 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 --- .../select/demoOptionGroups/index.html | 15 +++- .../select/demoOptionGroups/script.js | 8 +- src/components/select/select.js | 30 ++++++- src/components/select/select.spec.js | 86 +++++++++++++++++++ 4 files changed, 130 insertions(+), 9 deletions(-) diff --git a/src/components/select/demoOptionGroups/index.html b/src/components/select/demoOptionGroups/index.html index 8f3ac30f8d4..d0258a7aa9a 100644 --- a/src/components/select/demoOptionGroups/index.html +++ b/src/components/select/demoOptionGroups/index.html @@ -5,17 +5,26 @@

Pick your pizza below

- {{size}} + + {{size.name}} + + + {{size.name}} + - {{topping.name}} + + {{topping.name}} - {{topping.name}} + + {{topping.name}} diff --git a/src/components/select/demoOptionGroups/script.js b/src/components/select/demoOptionGroups/script.js index a16103c631e..f2cad9b0ff2 100644 --- a/src/components/select/demoOptionGroups/script.js +++ b/src/components/select/demoOptionGroups/script.js @@ -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' }, diff --git a/src/components/select/select.js b/src/components/select/select.js index f2889883937..90257bff195 100755 --- a/src/components/select/select.js +++ b/src/components/select/select.js @@ -741,6 +741,7 @@ function SelectMenuDirective($parse, $mdUtil, $mdConstant, $mdTheming) { return self.options; }, function() { self.ngModel.$render(); + updateOptionSetSizeAndPosition(); }); /** @@ -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); @@ -1371,6 +1395,7 @@ function OptgroupDirective() { if (!hasSelectHeader()) { setupLabelElement(); } + element.attr('role', 'group'); function hasSelectHeader() { return element.parent().find('md-select-header').length; @@ -1387,6 +1412,7 @@ function OptgroupDirective() { if (attrs.label) { labelElement.text(attrs.label); } + element.attr('aria-label', labelElement.text()); } } } @@ -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]; diff --git a/src/components/select/select.spec.js b/src/components/select/select.spec.js index 7d4ceebbaec..99c3cf428ba 100755 --- a/src/components/select/select.spec.js +++ b/src/components/select/select.spec.js @@ -1446,6 +1446,92 @@ describe('', 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( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' One' + + ' Two' + + ' Three' + + ' ' + + ' ' + + '')($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( + '' + + ' ' + + ' ' + + ' ' + + ' One' + + ' Two' + + ' Three' + + ' ' + + ' ' + + '')($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( + '' + + ' ' + + ' ' + + ' ' + + ' One' + + ' Two' + + ' Three' + + ' ' + + ' ' + + '')($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( + '' + + ' ' + + ' ' + + ' ' + + ' One' + + ' Two' + + ' Three' + + ' ' + + ' ' + + '')($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() {