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

Commit e56424e

Browse files
Splaktarmmalerba
authored andcommitted
fix(autocomplete): announce when an item is selected from the dropdown (#11403)
make sure that this message can be overridden in non-English locales fix some lint warnings and style issues add and fix JSDoc Fixes #10837
1 parent 1d73d81 commit e56424e

File tree

4 files changed

+86
-42
lines changed

4 files changed

+86
-42
lines changed

src/components/autocomplete/autocomplete.spec.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1733,15 +1733,41 @@ describe('<md-autocomplete>', function() {
17331733

17341734
ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.DOWN_ARROW));
17351735

1736-
// Flush twice, because the display value will be resolved asynchronously and then the live-announcer will
1737-
// be triggered.
1736+
// Flush twice, because the display value will be resolved asynchronously and then the
1737+
// live-announcer will be triggered.
17381738
$timeout.flush();
17391739
$timeout.flush();
17401740

17411741
expect(ctrl.index).toBe(1);
17421742
expect(liveEl.textContent).toBe(scope.items[1].display);
17431743
});
17441744

1745+
it('should announce when an option is selected', function() {
1746+
ctrl.focus();
1747+
waitForVirtualRepeat();
1748+
1749+
expect(ctrl.hidden).toBe(false);
1750+
1751+
ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.DOWN_ARROW));
1752+
1753+
// Flush twice, because the display value will be resolved asynchronously and then the
1754+
// live-announcer will be triggered.
1755+
$timeout.flush();
1756+
$timeout.flush();
1757+
1758+
expect(ctrl.index).toBe(0);
1759+
expect(liveEl.textContent).toBe(scope.items[0].display);
1760+
1761+
ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.ENTER));
1762+
1763+
// Flush twice, because the display value will be resolved asynchronously and then the
1764+
// live-announcer will be triggered.
1765+
$timeout.flush();
1766+
$timeout.flush();
1767+
1768+
expect(liveEl.textContent).toBe(scope.items[0].display + ' ' + ctrl.selectedMessage);
1769+
});
1770+
17451771
it('should announce the count when matches change', function() {
17461772

17471773
ctrl.focus();

src/components/autocomplete/demoBasicUsage/index.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@
2525
<md-checkbox ng-model="ctrl.simulateQuery">Simulate query for results?</md-checkbox>
2626
<md-checkbox ng-model="ctrl.noCache">Disable caching of queries?</md-checkbox>
2727
<md-checkbox ng-model="ctrl.isDisabled">Disable the input?</md-checkbox>
28-
29-
<p>By default, <code>md-autocomplete</code> will cache results when performing a query. After the initial call is performed, it will use the cached results to eliminate unnecessary server requests or lookup logic. This can be disabled above.</p>
28+
<p>
29+
By default, <code>md-autocomplete</code> will cache results when performing a query.
30+
After the initial call is performed, it will use the cached results to eliminate unnecessary
31+
server requests or lookup logic. This can be disabled above.
32+
</p>
3033
</form>
3134
</md-content>
3235
</div>

src/components/autocomplete/js/autocompleteController.js

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
4040
ctrl.isRequired = null;
4141
ctrl.isReadonly = null;
4242
ctrl.hasNotFound = false;
43+
ctrl.selectedMessage = $scope.selectedMessage || 'selected';
4344

4445
// Public Exported Methods
4546
ctrl.keydown = keydown;
@@ -321,8 +322,8 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
321322
if (!hidden && oldHidden) {
322323
positionDropdown();
323324

324-
// Report in polite mode, because the screenreader should finish the default description of
325-
// the input. element.
325+
// Report in polite mode, because the screen reader should finish the default description of
326+
// the input element.
326327
reportMessages(true, ReportType.Count | ReportType.Selected);
327328

328329
if (elements) {
@@ -406,14 +407,17 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
406407
});
407408
}
408409

409-
if (selectedItem !== previousSelectedItem) announceItemChange();
410+
if (selectedItem !== previousSelectedItem) {
411+
announceItemChange();
412+
}
410413
}
411414

412415
/**
413416
* Use the user-defined expression to announce changes each time a new item is selected
414417
*/
415418
function announceItemChange () {
416-
angular.isFunction($scope.itemChange) && $scope.itemChange(getItemAsNameVal($scope.selectedItem));
419+
angular.isFunction($scope.itemChange) &&
420+
$scope.itemChange(getItemAsNameVal($scope.selectedItem));
417421
}
418422

419423
/**
@@ -430,15 +434,17 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
430434
* @param previousSelectedItem
431435
*/
432436
function handleSelectedItemChange (selectedItem, previousSelectedItem) {
433-
selectedItemWatchers.forEach(function (watcher) { watcher(selectedItem, previousSelectedItem); });
437+
selectedItemWatchers.forEach(function (watcher) {
438+
watcher(selectedItem, previousSelectedItem);
439+
});
434440
}
435441

436442
/**
437443
* Register a function to be called when the selected item changes.
438444
* @param cb
439445
*/
440446
function registerSelectedItemWatcher (cb) {
441-
if (selectedItemWatchers.indexOf(cb) == -1) {
447+
if (selectedItemWatchers.indexOf(cb) === -1) {
442448
selectedItemWatchers.push(cb);
443449
}
444450
}
@@ -449,7 +455,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
449455
*/
450456
function unregisterSelectedItemWatcher (cb) {
451457
var i = selectedItemWatchers.indexOf(cb);
452-
if (i != -1) {
458+
if (i !== -1) {
453459
selectedItemWatchers.splice(i, 1);
454460
}
455461
}
@@ -472,9 +478,10 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
472478
if (searchText !== val) {
473479
$scope.selectedItem = null;
474480

475-
476481
// trigger change event if available
477-
if (searchText !== previousSearchText) announceTextChange();
482+
if (searchText !== previousSearchText) {
483+
announceTextChange();
484+
}
478485

479486
// cancel results if search text is not long enough
480487
if (!isMinLengthMet()) {
@@ -617,6 +624,8 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
617624
/**
618625
* Getter function to invoke user-defined expression (in the directive)
619626
* to convert your object to a single string.
627+
* @param item
628+
* @returns {string|null}
620629
*/
621630
function getItemText (item) {
622631
return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
@@ -626,20 +635,24 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
626635
/**
627636
* Returns the locals object for compiling item templates.
628637
* @param item
629-
* @returns {{}}
638+
* @returns {Object|undefined}
630639
*/
631640
function getItemAsNameVal (item) {
632-
if (!item) return undefined;
641+
if (!item) {
642+
return undefined;
643+
}
633644

634645
var locals = {};
635-
if (ctrl.itemName) locals[ ctrl.itemName ] = item;
646+
if (ctrl.itemName) {
647+
locals[ ctrl.itemName ] = item;
648+
}
636649

637650
return locals;
638651
}
639652

640653
/**
641654
* Returns the default index based on whether or not autoselect is enabled.
642-
* @returns {number}
655+
* @returns {number} 0 if autoselect is enabled, -1 if not.
643656
*/
644657
function getDefaultIndex () {
645658
return $scope.autoselect ? 0 : -1;
@@ -650,7 +663,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
650663
* @param value {boolean} Whether or not the component is currently loading.
651664
*/
652665
function setLoading(value) {
653-
if (ctrl.loading != value) {
666+
if (ctrl.loading !== value) {
654667
ctrl.loading = value;
655668
}
656669

@@ -718,40 +731,36 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
718731
}
719732

720733
/**
721-
* Returns true if the search text has matches.
722-
* @returns {boolean}
734+
* @returns {boolean} true if the search text has matches.
723735
*/
724736
function hasMatches() {
725737
return ctrl.matches.length ? true : false;
726738
}
727739

728740
/**
729-
* Returns true if the autocomplete has a valid selection.
730-
* @returns {boolean}
741+
* @returns {boolean} true if the autocomplete has a valid selection.
731742
*/
732743
function hasSelection() {
733744
return ctrl.scope.selectedItem ? true : false;
734745
}
735746

736747
/**
737-
* Returns true if the loading indicator is, or should be, visible.
738-
* @returns {boolean}
748+
* @returns {boolean} true if the loading indicator is, or should be, visible.
739749
*/
740750
function loadingIsVisible() {
741751
return ctrl.loading && !hasSelection();
742752
}
743753

744754
/**
745-
* Returns the display value of the current item.
746-
* @returns {*}
755+
* @returns {*} the display value of the current item.
747756
*/
748757
function getCurrentDisplayValue () {
749758
return getDisplayValue(ctrl.matches[ ctrl.index ]);
750759
}
751760

752761
/**
753762
* Determines if the minimum length is met by the search text.
754-
* @returns {*}
763+
* @returns {*} true if the minimum length is met by the search text
755764
*/
756765
function isMinLengthMet () {
757766
return ($scope.searchText || '').length >= getMinLength();
@@ -761,9 +770,9 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
761770

762771
/**
763772
* Defines a public property with a handler and a default value.
764-
* @param key
765-
* @param handler
766-
* @param value
773+
* @param {string} key
774+
* @param {Function} handler function
775+
* @param {*} value default value
767776
*/
768777
function defineProperty (key, handler, value) {
769778
Object.defineProperty(ctrl, key, {
@@ -778,13 +787,14 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
778787

779788
/**
780789
* Selects the item at the given index.
781-
* @param index
790+
* @param {number} index to select
782791
*/
783792
function select (index) {
784793
//-- force form to update state for validation
785794
$mdUtil.nextTick(function () {
786795
getDisplayValue(ctrl.matches[ index ]).then(function (val) {
787796
var ngModel = elements.$.input.controller('ngModel');
797+
$mdLiveAnnouncer.announce(val + ' ' + ctrl.selectedMessage, 'assertive');
788798
ngModel.$setViewValue(val);
789799
ngModel.$render();
790800
}).finally(function () {
@@ -884,12 +894,11 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
884894

885895

886896
/**
887-
* Reports given message types to supported screenreaders.
897+
* Reports given message types to supported screen readers.
888898
* @param {boolean} isPolite Whether the announcement should be polite.
889-
* @param {!number} types Message flags to be reported to the screenreader.
899+
* @param {!number} types Message flags to be reported to the screen reader.
890900
*/
891901
function reportMessages(isPolite, types) {
892-
893902
var politeness = isPolite ? 'polite' : 'assertive';
894903
var messages = [];
895904

@@ -904,12 +913,10 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
904913
$q.all(messages).then(function(data) {
905914
$mdLiveAnnouncer.announce(data.join(' '), politeness);
906915
});
907-
908916
}
909917

910918
/**
911-
* Returns the ARIA message for how many results match the current query.
912-
* @returns {*}
919+
* @returns {string} the ARIA message for how many results match the current query.
913920
*/
914921
function getCountMessage () {
915922
switch (ctrl.matches.length) {
@@ -1000,12 +1007,14 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
10001007
matches = ctrl.matches,
10011008
item = matches[ 0 ];
10021009
if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
1003-
var isMatching = searchText == displayValue;
1010+
var isMatching = searchText === displayValue;
10041011
if ($scope.matchInsensitive && !isMatching) {
1005-
isMatching = searchText.toLowerCase() == displayValue.toLowerCase();
1012+
isMatching = searchText.toLowerCase() === displayValue.toLowerCase();
10061013
}
10071014

1008-
if (isMatching) select(0);
1015+
if (isMatching) {
1016+
select(0);
1017+
}
10091018
});
10101019
}
10111020

src/components/autocomplete/js/autocompleteDirective.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,12 @@ angular
128128
* the dropdown.<br/><br/>
129129
* When the dropdown doesn't fit into the viewport, the dropdown will shrink
130130
* as much as possible.
131-
* @param {string=} md-dropdown-position Overrides the default dropdown position. Options: `top`, `bottom`.
131+
* @param {string=} md-dropdown-position Overrides the default dropdown position. Options: `top`,
132+
* `bottom`.
133+
* @param {string=} md-selected-message Attribute to specify the text that the screen reader will
134+
* announce after a value is selected. Default is: "selected". If `Alaska` is selected in the
135+
* options panel, it will read "Alaska selected". You will want to override this when your app
136+
* is running in a non-English locale.
132137
* @param {boolean=} ng-trim If set to false, the search text will be not trimmed automatically.
133138
* Defaults to true.
134139
* @param {string=} ng-pattern Adds the pattern validator to the ngModel of the search text.
@@ -272,7 +277,8 @@ function MdAutocomplete ($$mdSvgRegistry) {
272277
escapeOptions: '@?mdEscapeOptions',
273278
dropdownItems: '=?mdDropdownItems',
274279
dropdownPosition: '@?mdDropdownPosition',
275-
clearButton: '=?mdClearButton'
280+
clearButton: '=?mdClearButton',
281+
selectedMessage: '@?mdSelectedMessage'
276282
},
277283
compile: function(tElement, tAttrs) {
278284
var attributes = ['md-select-on-focus', 'md-no-asterisk', 'ng-trim', 'ng-pattern'];

0 commit comments

Comments
 (0)