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

Commit 534beea

Browse files
Splaktarandrewseguin
authored andcommitted
feat(autocomplete): add input-aria-label and input-aria-labelledby (#11412)
don't apply a duplicate aria-label and aria-labelledby to the same input change aria-describedby to input-aria-describedby Fixes #10815
1 parent 2b2f441 commit 534beea

File tree

7 files changed

+215
-23
lines changed

7 files changed

+215
-23
lines changed

src/components/autocomplete/autocomplete.spec.js

Lines changed: 163 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,8 +1660,170 @@ describe('<md-autocomplete>', function() {
16601660

16611661
});
16621662

1663-
describe('accessibility', function() {
1663+
describe('Accessibility', function() {
1664+
var $timeout = null;
16641665

1666+
beforeEach(inject(function ($injector) {
1667+
$timeout = $injector.get('$timeout');
1668+
}));
1669+
1670+
it('should add the placeholder as the input\'s aria-label', function() {
1671+
var template =
1672+
'<md-autocomplete' +
1673+
' md-selected-item="selectedItem"' +
1674+
' md-search-text="searchText"' +
1675+
' md-items="item in match(searchText)"' +
1676+
' md-item-text="item.display"' +
1677+
' placeholder="placeholder">' +
1678+
' <span md-highlight-text="searchText">{{item.display}}</span>' +
1679+
'</md-autocomplete>';
1680+
var scope = createScope();
1681+
var element = compile(template, scope);
1682+
var input = element.find('input');
1683+
1684+
// Flush the initial autocomplete timeout to gather the elements.
1685+
$timeout.flush();
1686+
1687+
expect(input.attr('aria-label')).toBe('placeholder');
1688+
});
1689+
1690+
it('should add the input-aria-label as the input\'s aria-label', function() {
1691+
var template =
1692+
'<md-autocomplete' +
1693+
' md-selected-item="selectedItem"' +
1694+
' md-search-text="searchText"' +
1695+
' md-items="item in match(searchText)"' +
1696+
' md-item-text="item.display"' +
1697+
' placeholder="placeholder"' +
1698+
' input-aria-label="TestLabel">' +
1699+
'</md-autocomplete>';
1700+
var scope = createScope();
1701+
var element = compile(template, scope);
1702+
var input = element.find('input');
1703+
1704+
// Flush the initial autocomplete timeout to gather the elements.
1705+
$timeout.flush();
1706+
1707+
expect(input.attr('aria-label')).toBe('TestLabel');
1708+
});
1709+
1710+
it('should add the input-aria-labelledby to the input', function() {
1711+
var template =
1712+
'<label id="test-label">Test Label</label>' +
1713+
'<md-autocomplete' +
1714+
' md-selected-item="selectedItem"' +
1715+
' md-search-text="searchText"' +
1716+
' md-items="item in match(searchText)"' +
1717+
' md-item-text="item.display"' +
1718+
' placeholder="placeholder"' +
1719+
' input-aria-labelledby="test-label">' +
1720+
'</md-autocomplete>';
1721+
var scope = createScope();
1722+
var element = compile(template, scope);
1723+
var input = element.find('input');
1724+
1725+
// Flush the initial autocomplete timeout to gather the elements.
1726+
$timeout.flush();
1727+
1728+
expect(input.attr('aria-label')).not.toExist();
1729+
expect(input.attr('aria-labelledby')).toBe('test-label');
1730+
});
1731+
1732+
it('should add the input-aria-describedby to the input', function() {
1733+
var template =
1734+
'<md-autocomplete' +
1735+
' md-selected-item="selectedItem"' +
1736+
' md-search-text="searchText"' +
1737+
' md-items="item in match(searchText)"' +
1738+
' md-item-text="item.display"' +
1739+
' placeholder="placeholder"' +
1740+
' input-aria-describedby="test-desc">' +
1741+
'</md-autocomplete>' +
1742+
'<div id="test-desc">Test Description</div>';
1743+
var scope = createScope();
1744+
var element = compile(template, scope);
1745+
var input = element.find('input');
1746+
1747+
// Flush the initial autocomplete timeout to gather the elements.
1748+
$timeout.flush();
1749+
1750+
expect(input.attr('aria-describedby')).toBe('test-desc');
1751+
});
1752+
1753+
it('should not break an aria-label on the autocomplete when using input-aria-label or aria-describedby', function() {
1754+
var template =
1755+
'<md-autocomplete' +
1756+
' md-selected-item="selectedItem"' +
1757+
' md-search-text="searchText"' +
1758+
' md-items="item in match(searchText)"' +
1759+
' md-item-text="item.display"' +
1760+
' placeholder="placeholder"' +
1761+
' aria-label="TestAriaLabel"' +
1762+
' input-aria-label="TestLabel"' +
1763+
' input-aria-describedby="test-desc">' +
1764+
'</md-autocomplete>' +
1765+
'<div id="test-desc">Test Description</div>';
1766+
var scope = createScope();
1767+
var element = compile(template, scope);
1768+
var autocomplete = element[0];
1769+
var input = element.find('input');
1770+
1771+
// Flush the initial autocomplete timeout to gather the elements.
1772+
$timeout.flush();
1773+
1774+
expect(input.attr('aria-label')).toBe('TestLabel');
1775+
expect(input.attr('aria-describedby')).toBe('test-desc');
1776+
expect(autocomplete.getAttribute('aria-label')).toBe('TestAriaLabel');
1777+
});
1778+
1779+
it('should not break an aria-label on the autocomplete', function() {
1780+
var template =
1781+
'<md-autocomplete' +
1782+
' md-selected-item="selectedItem"' +
1783+
' md-search-text="searchText"' +
1784+
' md-items="item in match(searchText)"' +
1785+
' md-item-text="item.display"' +
1786+
' placeholder="placeholder"' +
1787+
' aria-label="TestAriaLabel">' +
1788+
'</md-autocomplete>';
1789+
var scope = createScope();
1790+
var element = compile(template, scope);
1791+
var input = element.find('input');
1792+
1793+
// Flush the initial autocomplete timeout to gather the elements.
1794+
$timeout.flush();
1795+
1796+
expect(input.attr('aria-label')).toBe('placeholder');
1797+
expect(element.attr('aria-label')).toBe('TestAriaLabel');
1798+
});
1799+
1800+
it('should not break an aria-label on the autocomplete when using input-aria-labelledby', function() {
1801+
var template =
1802+
'<label id="test-label">Test Label</label>' +
1803+
'<md-autocomplete' +
1804+
' md-selected-item="selectedItem"' +
1805+
' md-search-text="searchText"' +
1806+
' md-items="item in match(searchText)"' +
1807+
' md-item-text="item.display"' +
1808+
' placeholder="placeholder"' +
1809+
' aria-label="TestAriaLabel"' +
1810+
' input-aria-labelledby="test-label">' +
1811+
'</md-autocomplete>';
1812+
var scope = createScope();
1813+
var element = compile(template, scope);
1814+
var autocomplete = element[1];
1815+
var input = element.find('input');
1816+
1817+
// Flush the initial autocomplete timeout to gather the elements.
1818+
$timeout.flush();
1819+
1820+
expect(input.attr('aria-label')).not.toExist();
1821+
expect(input.attr('aria-labelledby')).toBe('test-label');
1822+
expect(autocomplete.getAttribute('aria-label')).toBe('TestAriaLabel');
1823+
});
1824+
});
1825+
1826+
describe('Accessibility Announcements', function() {
16651827
var $mdLiveAnnouncer, $timeout, $mdConstant = null;
16661828
var liveEl, scope, element, ctrl = null;
16671829

@@ -1691,7 +1853,6 @@ describe('<md-autocomplete>', function() {
16911853
}));
16921854

16931855
it('should announce count on dropdown open', function() {
1694-
16951856
ctrl.focus();
16961857
waitForVirtualRepeat();
16971858

@@ -1701,7 +1862,6 @@ describe('<md-autocomplete>', function() {
17011862
});
17021863

17031864
it('should announce count and selection on dropdown open', function() {
1704-
17051865
// Manually enable md-autoselect for the autocomplete.
17061866
ctrl.index = 0;
17071867

@@ -1715,7 +1875,6 @@ describe('<md-autocomplete>', function() {
17151875
});
17161876

17171877
it('should announce the selection when using the arrow keys', function() {
1718-
17191878
ctrl.focus();
17201879
waitForVirtualRepeat();
17211880

@@ -1769,7 +1928,6 @@ describe('<md-autocomplete>', function() {
17691928
});
17701929

17711930
it('should announce the count when matches change', function() {
1772-
17731931
ctrl.focus();
17741932
waitForVirtualRepeat();
17751933

src/components/autocomplete/demoBasicUsage/index.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<p id="autocompleteDescription">
55
Use <code>md-autocomplete</code> to search for matches from local or remote data sources.
66
</p>
7+
<label id="favoriteStateLabel">Favorite State</label>
78
<md-autocomplete
89
ng-disabled="ctrl.isDisabled"
910
md-no-cache="ctrl.noCache"
@@ -14,8 +15,9 @@
1415
md-items="item in ctrl.querySearch(ctrl.searchText)"
1516
md-item-text="item.display"
1617
md-min-length="0"
17-
placeholder="What is your favorite US state?"
18-
aria-describedby="autocompleteDescription autocompleteDetailedDescription">
18+
placeholder="Ex. Alaska"
19+
input-aria-labelledby="favoriteStateLabel"
20+
input-aria-describedby="autocompleteDetailedDescription">
1921
<md-item-template>
2022
<span md-highlight-text="ctrl.searchText" md-highlight-flags="^i">{{item.display}}</span>
2123
</md-item-template>

src/components/autocomplete/demoCustomTemplate/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
md-items="item in ctrl.querySearch(ctrl.searchText)"
1414
md-item-text="item.name"
1515
md-min-length="0"
16+
input-aria-label="Current Repository"
1617
placeholder="Pick an Angular repository"
1718
md-menu-class="autocomplete-custom-template"
1819
md-menu-container-class="custom-container">

src/components/autocomplete/demoFloatingLabel/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
md-item-text="item.display"
1919
md-require-match=""
2020
md-floating-label="Favorite state"
21-
aria-describedby="favoriteStateDescription">
21+
input-aria-describedby="favoriteStateDescription">
2222
<md-item-template>
2323
<span md-highlight-text="ctrl.searchText">{{item.display}}</span>
2424
</md-item-template>

src/components/autocomplete/demoInsideDialog/dialog.tmpl.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@ <h2>Autocomplete Dialog Example</h2>
1313
<md-dialog-content ng-cloak>
1414
<div class="md-dialog-content">
1515
<form ng-submit="$event.preventDefault()">
16-
<p>Use <code>md-autocomplete</code> to search for matches from local or remote data sources.</p>
16+
<p>
17+
Use <code>md-autocomplete</code> to search for matches from local or remote data sources.
18+
</p>
1719
<md-autocomplete
1820
md-selected-item="ctrl.selectedItem"
1921
md-search-text="ctrl.searchText"
2022
md-items="item in ctrl.querySearch(ctrl.searchText)"
2123
md-item-text="item.display"
2224
md-min-length="0"
2325
placeholder="What is your favorite US state?"
26+
input-aria-label="Favorite State"
2427
md-autofocus="">
2528
<md-item-template>
2629
<span md-highlight-text="ctrl.searchText" md-highlight-flags="^i">{{item.display}}</span>

src/components/autocomplete/js/autocompleteController.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
9696
if ($scope.inputAriaDescribedBy) {
9797
elements.input.setAttribute('aria-describedby', $scope.inputAriaDescribedBy);
9898
}
99+
if (!$scope.floatingLabel) {
100+
if ($scope.inputAriaLabel) {
101+
elements.input.setAttribute('aria-label', $scope.inputAriaLabel);
102+
} else if ($scope.inputAriaLabelledBy) {
103+
elements.input.setAttribute('aria-labelledby', $scope.inputAriaLabelledBy);
104+
} else if ($scope.placeholder) {
105+
// If no aria-label or aria-labelledby references are defined, then just label using the
106+
// placeholder.
107+
elements.input.setAttribute('aria-label', $scope.placeholder);
108+
}
109+
}
99110
});
100111
}
101112

src/components/autocomplete/js/autocompleteDirective.js

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ angular
1919
* no matches were found. You can do this by wrapping your template in `md-item-template` and
2020
* adding a tag for `md-not-found`. An example of this is shown below.
2121
*
22-
* To reset the displayed value you must clear both values for `md-search-text` and `md-selected-item`.
22+
* To reset the displayed value you must clear both values for `md-search-text` and
23+
* `md-selected-item`.
2324
*
2425
* ### Validation
2526
*
@@ -96,11 +97,14 @@ angular
9697
* make suggestions.
9798
* @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
9899
* for results.
99-
* @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show up or not.
100-
* @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a `$mdDialog`,
101-
* `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening. <br/><br/>
100+
* @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show
101+
* up or not.
102+
* @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a
103+
* `$mdDialog`, `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening.
104+
* <br/><br/>
102105
* Also the autocomplete will immediately focus the input element.
103-
* @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating label.
106+
* @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating
107+
* label.
104108
* @param {boolean=} md-autoselect If set to true, the first item will be automatically selected
105109
* in the dropdown upon open.
106110
* @param {string=} md-input-name The name attribute given to the input element to be used with
@@ -111,7 +115,7 @@ angular
111115
* @param {string=} md-input-class This will be applied to the input for styling. This attribute
112116
* is only valid when a `md-floating-label` is defined.
113117
* @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
114-
* `md-input-container`
118+
* `md-input-container`.
115119
* @param {string=} md-select-on-focus When present the inputs text will be automatically selected
116120
* on focus.
117121
* @param {string=} md-input-id An ID to be added to the input element.
@@ -135,6 +139,15 @@ angular
135139
* content of these elements at the end of announcing that the autocomplete has been selected
136140
* and describing its current state. The descriptive elements do not need to be visible on the
137141
* page.
142+
* @param {string=} input-aria-labelledby A space-separated list of element IDs. The ideal use case
143+
* is that this would contain the ID of a `<label>` element that is associated with this
144+
* autocomplete. This will only have affect when `md-floating-label` is not defined.<br><br>
145+
* For `<label id="state">US State</label>`, you would set this to
146+
* `input-aria-labelledby="state"`.
147+
* @param {string=} input-aria-label A label that will be applied to the autocomplete's input.
148+
* This will be announced by screen readers before the placeholder.
149+
* This will only have affect when `md-floating-label` is not defined. If you define both
150+
* `input-aria-label` and `input-aria-labelledby`, then `input-aria-label` will take precedence.
138151
* @param {string=} md-selected-message Attribute to specify the text that the screen reader will
139152
* announce after a value is selected. Default is: "selected". If `Alaska` is selected in the
140153
* options panel, it will read "Alaska selected". You will want to override this when your app
@@ -179,12 +192,12 @@ angular
179192
* ### Clear button for the input
180193
* By default, the clear button is displayed when there is input. This aligns with the spec's
181194
* [Search Pattern](https://material.io/archive/guidelines/patterns/search.html#search-in-app-search).
182-
* In floating label mode, when `md-floating-label="My Label"` is applied, the clear button is not displayed
183-
* by default (see the spec's
195+
* In floating label mode, when `md-floating-label="My Label"` is applied, the clear button is not
196+
* displayed by default (see the spec's
184197
* [Autocomplete Text Field](https://material.io/archive/guidelines/components/text-fields.html#text-fields-layout)).
185198
*
186-
* Nevertheless, developers are able to explicitly toggle the clear button for all autocomplete components
187-
* with `md-clear-button`.
199+
* Nevertheless, developers are able to explicitly toggle the clear button for all autocomplete
200+
* components with `md-clear-button`.
188201
*
189202
* <hljs lang="html">
190203
* <md-autocomplete ... md-clear-button="true"></md-autocomplete>
@@ -223,7 +236,8 @@ angular
223236
* input validation for the field.
224237
*
225238
* ### Asynchronous Results
226-
* The autocomplete items expression also supports promises, which will resolve with the query results.
239+
* The autocomplete items expression also supports promises, which will resolve with the query
240+
* results.
227241
*
228242
* <hljs lang="js">
229243
* function AppController($scope, $http) {
@@ -265,6 +279,8 @@ function MdAutocomplete ($$mdSvgRegistry) {
265279
itemText: '&mdItemText',
266280
placeholder: '@placeholder',
267281
inputAriaDescribedBy: '@?inputAriaDescribedby',
282+
inputAriaLabelledBy: '@?inputAriaLabelledby',
283+
inputAriaLabel: '@?inputAriaLabel',
268284
noCache: '=?mdNoCache',
269285
requireMatch: '=?mdRequireMatch',
270286
selectOnMatch: '=?mdSelectOnMatch',
@@ -303,8 +319,8 @@ function MdAutocomplete ($$mdSvgRegistry) {
303319
// be added to the element in the template function.
304320
ctrl.hasNotFound = !!element.attr('md-has-not-found');
305321

306-
// By default the inset autocomplete should show the clear button when not explicitly overwritten
307-
// or in floating label mode.
322+
// By default the inset autocomplete should show the clear button when not explicitly
323+
// overwritten or in floating label mode.
308324
if (!angular.isDefined(attrs.mdClearButton) && !scope.floatingLabel) {
309325
scope.clearButton = true;
310326
}
@@ -370,7 +386,8 @@ function MdAutocomplete ($$mdSvgRegistry) {
370386
var templateTag = element.find('md-item-template').detach(),
371387
html = templateTag.length ? templateTag.html() : element.html();
372388
if (!templateTag.length) element.empty();
373-
return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html + '</md-autocomplete-parent-scope>';
389+
return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html +
390+
'</md-autocomplete-parent-scope>';
374391
}
375392

376393
function getNoItemsTemplate() {

0 commit comments

Comments
 (0)