From 3fdafd0841b0ef7d58a6b6de7f538d8503d9ac8e Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 11 Aug 2018 17:35:54 -0400 Subject: [PATCH] feat(autocomplete): add input-aria-label and input-aria-labelledby don't apply a duplicate aria-label and aria-labelledby to the same input change aria-describedby to input-aria-describedby Fixes #10815 --- .../autocomplete/autocomplete.spec.js | 168 +++++++++++++++++- .../autocomplete/demoBasicUsage/index.html | 6 +- .../demoCustomTemplate/index.html | 1 + .../autocomplete/demoFloatingLabel/index.html | 2 +- .../demoInsideDialog/dialog.tmpl.html | 5 +- .../autocomplete/js/autocompleteController.js | 11 ++ .../autocomplete/js/autocompleteDirective.js | 45 +++-- 7 files changed, 215 insertions(+), 23 deletions(-) diff --git a/src/components/autocomplete/autocomplete.spec.js b/src/components/autocomplete/autocomplete.spec.js index 793cef9447..5e81aec86a 100644 --- a/src/components/autocomplete/autocomplete.spec.js +++ b/src/components/autocomplete/autocomplete.spec.js @@ -1660,8 +1660,170 @@ describe('', function() { }); - describe('accessibility', function() { + describe('Accessibility', function() { + var $timeout = null; + beforeEach(inject(function ($injector) { + $timeout = $injector.get('$timeout'); + })); + + it('should add the placeholder as the input\'s aria-label', function() { + var template = + '' + + ' {{item.display}}' + + ''; + var scope = createScope(); + var element = compile(template, scope); + var input = element.find('input'); + + // Flush the initial autocomplete timeout to gather the elements. + $timeout.flush(); + + expect(input.attr('aria-label')).toBe('placeholder'); + }); + + it('should add the input-aria-label as the input\'s aria-label', function() { + var template = + '' + + ''; + var scope = createScope(); + var element = compile(template, scope); + var input = element.find('input'); + + // Flush the initial autocomplete timeout to gather the elements. + $timeout.flush(); + + expect(input.attr('aria-label')).toBe('TestLabel'); + }); + + it('should add the input-aria-labelledby to the input', function() { + var template = + '' + + '' + + ''; + var scope = createScope(); + var element = compile(template, scope); + var input = element.find('input'); + + // Flush the initial autocomplete timeout to gather the elements. + $timeout.flush(); + + expect(input.attr('aria-label')).not.toExist(); + expect(input.attr('aria-labelledby')).toBe('test-label'); + }); + + it('should add the input-aria-describedby to the input', function() { + var template = + '' + + '' + + '
Test Description
'; + var scope = createScope(); + var element = compile(template, scope); + var input = element.find('input'); + + // Flush the initial autocomplete timeout to gather the elements. + $timeout.flush(); + + expect(input.attr('aria-describedby')).toBe('test-desc'); + }); + + it('should not break an aria-label on the autocomplete when using input-aria-label or aria-describedby', function() { + var template = + '' + + '' + + '
Test Description
'; + var scope = createScope(); + var element = compile(template, scope); + var autocomplete = element[0]; + var input = element.find('input'); + + // Flush the initial autocomplete timeout to gather the elements. + $timeout.flush(); + + expect(input.attr('aria-label')).toBe('TestLabel'); + expect(input.attr('aria-describedby')).toBe('test-desc'); + expect(autocomplete.getAttribute('aria-label')).toBe('TestAriaLabel'); + }); + + it('should not break an aria-label on the autocomplete', function() { + var template = + '' + + ''; + var scope = createScope(); + var element = compile(template, scope); + var input = element.find('input'); + + // Flush the initial autocomplete timeout to gather the elements. + $timeout.flush(); + + expect(input.attr('aria-label')).toBe('placeholder'); + expect(element.attr('aria-label')).toBe('TestAriaLabel'); + }); + + it('should not break an aria-label on the autocomplete when using input-aria-labelledby', function() { + var template = + '' + + '' + + ''; + var scope = createScope(); + var element = compile(template, scope); + var autocomplete = element[1]; + var input = element.find('input'); + + // Flush the initial autocomplete timeout to gather the elements. + $timeout.flush(); + + expect(input.attr('aria-label')).not.toExist(); + expect(input.attr('aria-labelledby')).toBe('test-label'); + expect(autocomplete.getAttribute('aria-label')).toBe('TestAriaLabel'); + }); + }); + + describe('Accessibility Announcements', function() { var $mdLiveAnnouncer, $timeout, $mdConstant = null; var liveEl, scope, element, ctrl = null; @@ -1691,7 +1853,6 @@ describe('', function() { })); it('should announce count on dropdown open', function() { - ctrl.focus(); waitForVirtualRepeat(); @@ -1701,7 +1862,6 @@ describe('', function() { }); it('should announce count and selection on dropdown open', function() { - // Manually enable md-autoselect for the autocomplete. ctrl.index = 0; @@ -1715,7 +1875,6 @@ describe('', function() { }); it('should announce the selection when using the arrow keys', function() { - ctrl.focus(); waitForVirtualRepeat(); @@ -1769,7 +1928,6 @@ describe('', function() { }); it('should announce the count when matches change', function() { - ctrl.focus(); waitForVirtualRepeat(); diff --git a/src/components/autocomplete/demoBasicUsage/index.html b/src/components/autocomplete/demoBasicUsage/index.html index bd6b844e50..e5ac536ef2 100644 --- a/src/components/autocomplete/demoBasicUsage/index.html +++ b/src/components/autocomplete/demoBasicUsage/index.html @@ -4,6 +4,7 @@

Use md-autocomplete to search for matches from local or remote data sources.

+ + placeholder="Ex. Alaska" + input-aria-labelledby="favoriteStateLabel" + input-aria-describedby="autocompleteDetailedDescription"> {{item.display}} diff --git a/src/components/autocomplete/demoCustomTemplate/index.html b/src/components/autocomplete/demoCustomTemplate/index.html index 6f26a9637e..0074a57a81 100644 --- a/src/components/autocomplete/demoCustomTemplate/index.html +++ b/src/components/autocomplete/demoCustomTemplate/index.html @@ -13,6 +13,7 @@ md-items="item in ctrl.querySearch(ctrl.searchText)" md-item-text="item.name" md-min-length="0" + input-aria-label="Current Repository" placeholder="Pick an Angular repository" md-menu-class="autocomplete-custom-template" md-menu-container-class="custom-container"> diff --git a/src/components/autocomplete/demoFloatingLabel/index.html b/src/components/autocomplete/demoFloatingLabel/index.html index c3bb1a6da6..03be8b4810 100644 --- a/src/components/autocomplete/demoFloatingLabel/index.html +++ b/src/components/autocomplete/demoFloatingLabel/index.html @@ -18,7 +18,7 @@ md-item-text="item.display" md-require-match="" md-floating-label="Favorite state" - aria-describedby="favoriteStateDescription"> + input-aria-describedby="favoriteStateDescription"> {{item.display}} diff --git a/src/components/autocomplete/demoInsideDialog/dialog.tmpl.html b/src/components/autocomplete/demoInsideDialog/dialog.tmpl.html index 92bf3708b4..d66473db3a 100644 --- a/src/components/autocomplete/demoInsideDialog/dialog.tmpl.html +++ b/src/components/autocomplete/demoInsideDialog/dialog.tmpl.html @@ -13,7 +13,9 @@

Autocomplete Dialog Example

-

Use md-autocomplete to search for matches from local or remote data sources.

+

+ Use md-autocomplete to search for matches from local or remote data sources. +

Autocomplete Dialog Example md-item-text="item.display" md-min-length="0" placeholder="What is your favorite US state?" + input-aria-label="Favorite State" md-autofocus=""> {{item.display}} diff --git a/src/components/autocomplete/js/autocompleteController.js b/src/components/autocomplete/js/autocompleteController.js index 05cfc13b44..c3bdac7429 100644 --- a/src/components/autocomplete/js/autocompleteController.js +++ b/src/components/autocomplete/js/autocompleteController.js @@ -96,6 +96,17 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, if ($scope.inputAriaDescribedBy) { elements.input.setAttribute('aria-describedby', $scope.inputAriaDescribedBy); } + if (!$scope.floatingLabel) { + if ($scope.inputAriaLabel) { + elements.input.setAttribute('aria-label', $scope.inputAriaLabel); + } else if ($scope.inputAriaLabelledBy) { + elements.input.setAttribute('aria-labelledby', $scope.inputAriaLabelledBy); + } else if ($scope.placeholder) { + // If no aria-label or aria-labelledby references are defined, then just label using the + // placeholder. + elements.input.setAttribute('aria-label', $scope.placeholder); + } + } }); } diff --git a/src/components/autocomplete/js/autocompleteDirective.js b/src/components/autocomplete/js/autocompleteDirective.js index a0a8c6d712..f3d362518b 100644 --- a/src/components/autocomplete/js/autocompleteDirective.js +++ b/src/components/autocomplete/js/autocompleteDirective.js @@ -19,7 +19,8 @@ angular * no matches were found. You can do this by wrapping your template in `md-item-template` and * adding a tag for `md-not-found`. An example of this is shown below. * - * To reset the displayed value you must clear both values for `md-search-text` and `md-selected-item`. + * To reset the displayed value you must clear both values for `md-search-text` and + * `md-selected-item`. * * ### Validation * @@ -96,11 +97,14 @@ angular * make suggestions. * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking * for results. - * @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show up or not. - * @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a `$mdDialog`, - * `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening.

+ * @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show + * up or not. + * @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a + * `$mdDialog`, `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening. + *

* Also the autocomplete will immediately focus the input element. - * @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating label. + * @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating + * label. * @param {boolean=} md-autoselect If set to true, the first item will be automatically selected * in the dropdown upon open. * @param {string=} md-input-name The name attribute given to the input element to be used with @@ -111,7 +115,7 @@ angular * @param {string=} md-input-class This will be applied to the input for styling. This attribute * is only valid when a `md-floating-label` is defined. * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in - * `md-input-container` + * `md-input-container`. * @param {string=} md-select-on-focus When present the inputs text will be automatically selected * on focus. * @param {string=} md-input-id An ID to be added to the input element. @@ -135,6 +139,15 @@ angular * content of these elements at the end of announcing that the autocomplete has been selected * and describing its current state. The descriptive elements do not need to be visible on the * page. + * @param {string=} input-aria-labelledby A space-separated list of element IDs. The ideal use case + * is that this would contain the ID of a `