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

Commit e65ffc8

Browse files
devversionhansl
authored andcommitted
fix(autocomplete): properly show dropdown on focus when minlength is met. (#9291)
Currently the `md-autocomplete` shows its dropdown only by changing the `searchText` from `null` to an empty string. > This triggered a searchText change, which resulted in triggering a query. This approach doesn't seem to be very elegant. > The autocomplete should manually trigger a query and possibly open the dropdown if matches are available. Fixes #9283. Closes #9288. Closes #9289.
1 parent 904b455 commit e65ffc8

File tree

3 files changed

+128
-19
lines changed

3 files changed

+128
-19
lines changed

src/components/autocomplete/autocomplete.spec.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ describe('<md-autocomplete>', function() {
4141

4242
scope.searchText = '';
4343
scope.selectedItem = null;
44+
scope.items = items;
4445

4546
angular.forEach(scopeData, function(value, key) {
4647
scope[key] = value;
@@ -1841,6 +1842,100 @@ describe('<md-autocomplete>', function() {
18411842
document.body.removeChild(parent[0]);
18421843
}));
18431844

1845+
it('should show on focus when min-length is met', inject(function($timeout) {
1846+
var scope = createScope();
1847+
1848+
// Overwrite the match function to always show some results.
1849+
scope.match = function() {
1850+
return scope.items;
1851+
};
1852+
1853+
var template =
1854+
'<div style="width: 400px">' +
1855+
'<md-autocomplete ' +
1856+
'md-search-text="searchText" ' +
1857+
'md-items="item in match(searchText)" ' +
1858+
'md-item-text="item.display" ' +
1859+
'md-min-length="0" ' +
1860+
'placeholder="placeholder">' +
1861+
'<span md-highlight-text="searchText">{{item.display}}</span>' +
1862+
'</md-autocomplete>' +
1863+
'</div>';
1864+
1865+
var parent = compile(template, scope);
1866+
var element = parent.find('md-autocomplete');
1867+
var ctrl = element.controller('mdAutocomplete');
1868+
1869+
// Add container to the DOM to be able to test the rect calculations.
1870+
document.body.appendChild(parent[0]);
1871+
1872+
ctrl.focus();
1873+
waitForVirtualRepeat(element);
1874+
1875+
// The scroll repeat container has been moved to the body element to avoid
1876+
// z-index / overflow issues.
1877+
var scrollContainer = document.body.querySelector('.md-virtual-repeat-container');
1878+
expect(scrollContainer).toBeTruthy();
1879+
1880+
// Expect the current width of the scrollContainer to be the same as of the parent element
1881+
// at initialization.
1882+
expect(scrollContainer.offsetParent).toBeTruthy();
1883+
1884+
document.body.removeChild(parent[0]);
1885+
}));
1886+
1887+
it('should not show on focus when min-length is not met', inject(function($timeout) {
1888+
var scope = createScope();
1889+
1890+
// Overwrite the match function to always show some results.
1891+
scope.match = function() {
1892+
return scope.items;
1893+
};
1894+
1895+
var template =
1896+
'<div style="width: 400px">' +
1897+
'<md-autocomplete ' +
1898+
'md-search-text="searchText" ' +
1899+
'md-items="item in match(searchText)" ' +
1900+
'md-item-text="item.display" ' +
1901+
'md-min-length="1" ' +
1902+
'placeholder="placeholder">' +
1903+
'<span md-highlight-text="searchText">{{item.display}}</span>' +
1904+
'</md-autocomplete>' +
1905+
'</div>';
1906+
1907+
var parent = compile(template, scope);
1908+
var element = parent.find('md-autocomplete');
1909+
var ctrl = element.controller('mdAutocomplete');
1910+
1911+
// Add container to the DOM to be able to test the rect calculations.
1912+
document.body.appendChild(parent[0]);
1913+
1914+
ctrl.focus();
1915+
waitForVirtualRepeat(element);
1916+
1917+
// The scroll repeat container has been moved to the body element to avoid
1918+
// z-index / overflow issues.
1919+
var scrollContainer = document.body.querySelector('.md-virtual-repeat-container');
1920+
expect(scrollContainer).toBeTruthy();
1921+
1922+
// Expect the dropdown to not show up, because the min-length is not met.
1923+
expect(scrollContainer.offsetParent).toBeFalsy();
1924+
1925+
ctrl.blur();
1926+
1927+
// Add one char to the searchText to match the minlength.
1928+
scope.$apply('searchText = "X"');
1929+
1930+
ctrl.focus();
1931+
waitForVirtualRepeat(element);
1932+
1933+
// Expect the dropdown to not show up, because the min-length is not met.
1934+
expect(scrollContainer.offsetParent).toBeTruthy();
1935+
1936+
document.body.removeChild(parent[0]);
1937+
}));
1938+
18441939
});
18451940

18461941
describe('md-highlight-text', function() {

src/components/autocomplete/js/autocompleteController.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -453,17 +453,12 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
453453
function focus($event) {
454454
hasFocus = true;
455455

456-
// When the searchText is not a string, force it to be an empty string.
457-
if (!angular.isString($scope.searchText)) {
458-
$scope.searchText = '';
456+
if (isSearchable() && isMinLengthMet()) {
457+
handleQuery();
459458
}
460459

461460
ctrl.hidden = shouldHide();
462461

463-
if (!ctrl.hidden) {
464-
handleQuery();
465-
}
466-
467462
evalAttr('ngFocus', { $event: $event });
468463
}
469464

@@ -600,10 +595,19 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
600595
* @returns {boolean}
601596
*/
602597
function shouldHide () {
603-
if (ctrl.loading && !hasMatches()) return true; // Hide while loading initial matches
604-
else if (hasSelection()) return true; // Hide if there is already a selection
605-
else if (!hasFocus) return true; // Hide if the input does not have focus
606-
else return !shouldShow(); // Defer to standard show logic
598+
if (!isSearchable()) return true; // Hide when not able to query
599+
else return !shouldShow(); // Hide when the dropdown is not able to show.
600+
}
601+
602+
/**
603+
* Determines whether the autocomplete is able to query within the current state.
604+
* @returns {boolean}
605+
*/
606+
function isSearchable() {
607+
if (ctrl.loading && !hasMatches()) return false; // No query when query is in progress.
608+
else if (hasSelection()) return false; // No query if there is already a selection
609+
else if (!hasFocus) return false; // No query if the input does not have focus
610+
return true;
607611
}
608612

609613
/**

src/components/chips/js/chipsController.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $md
4141
/** @type {angular.NgModelController} */
4242
this.userInputNgModelCtrl = null;
4343

44+
/** @type {MdAutocompleteCtrl} */
45+
this.autocompleteCtrl = null;
46+
4447
/** @type {Element} */
4548
this.userInputElement = null;
4649

@@ -50,9 +53,6 @@ function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $md
5053
/** @type {number} */
5154
this.selectedChip = -1;
5255

53-
/** @type {boolean} */
54-
this.hasAutocomplete = false;
55-
5656
/** @type {string} */
5757
this.enableChipEdit = $mdUtil.parseAttributeBoolean($attrs.mdEnableChipEdit);
5858

@@ -113,7 +113,7 @@ MdChipsCtrl.prototype.inputKeydown = function(event) {
113113
var chipBuffer = this.getChipBuffer();
114114

115115
// If we have an autocomplete, and it handled the event, we have nothing to do
116-
if (this.hasAutocomplete && event.isDefaultPrevented && event.isDefaultPrevented()) {
116+
if (this.autocompleteCtrl && event.isDefaultPrevented && event.isDefaultPrevented()) {
117117
return;
118118
}
119119

@@ -141,7 +141,7 @@ MdChipsCtrl.prototype.inputKeydown = function(event) {
141141

142142
// Support additional separator key codes in an array of `md-separator-keys`.
143143
if (this.separatorKeys.indexOf(event.keyCode) !== -1) {
144-
if ((this.hasAutocomplete && this.requireMatch) || !chipBuffer) return;
144+
if ((this.autocompleteCtrl && this.requireMatch) || !chipBuffer) return;
145145
event.preventDefault();
146146

147147
// Only append the chip and reset the chip buffer if the max chips limit isn't reached.
@@ -435,7 +435,17 @@ MdChipsCtrl.prototype.removeChip = function(index) {
435435

436436
MdChipsCtrl.prototype.removeChipAndFocusInput = function (index) {
437437
this.removeChip(index);
438-
this.onFocus();
438+
439+
if (this.autocompleteCtrl) {
440+
// Always hide the autocomplete dropdown before focusing the autocomplete input.
441+
// Wait for the input to move horizontally, because the chip was removed.
442+
// This can lead to an incorrect dropdown position.
443+
this.autocompleteCtrl.hidden = true;
444+
this.$mdUtil.nextTick(this.onFocus.bind(this));
445+
} else {
446+
this.onFocus();
447+
}
448+
439449
};
440450
/**
441451
* Selects the chip at `index`,
@@ -567,8 +577,8 @@ MdChipsCtrl.prototype.configureUserInput = function(inputElement) {
567577
};
568578

569579
MdChipsCtrl.prototype.configureAutocomplete = function(ctrl) {
570-
if ( ctrl ) {
571-
this.hasAutocomplete = true;
580+
if (ctrl) {
581+
this.autocompleteCtrl = ctrl;
572582

573583
ctrl.registerSelectedItemWatcher(angular.bind(this, function (item) {
574584
if (item) {

0 commit comments

Comments
 (0)