diff --git a/src/typeahead/docs/readme.md b/src/typeahead/docs/readme.md index 1fcb47fb43..621415c565 100644 --- a/src/typeahead/docs/readme.md +++ b/src/typeahead/docs/readme.md @@ -51,3 +51,7 @@ The typeahead directives provide several attributes: * `typeahead-wait-ms` _(Defaults: 0)_ : Minimal wait time after last character typed before typeahead kicks-in + +* `typeahead-focus-first` + _(Defaults: true)_ : + Should the first match automatically be focused as you type? diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index de9a385d3f..8f2c7323c1 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -80,7 +80,12 @@ describe('typeahead tests', function () { this.message = function () { return 'Expected "' + this.actual + '" to be opened.'; }; - return typeaheadEl.length === 1 && typeaheadEl.hasClass('ng-hide') === false && liEls.length === noOfMatches && $(liEls[activeIdx]).hasClass('active'); + + return (typeaheadEl.length === 1 && + typeaheadEl.hasClass('ng-hide') === false && + liEls.length === noOfMatches && + (activeIdx === -1 ? !$(liEls).hasClass('active') : $(liEls[activeIdx]).hasClass('active')) + ); } }); }); @@ -409,7 +414,7 @@ describe('typeahead tests', function () { triggerKeyDown(element, 38); expect(element).toBeOpenWithActive(2, 1); - // Up arrow key goes back to last element + // Up arrow key goes back to first element triggerKeyDown(element, 38); expect(element).toBeOpenWithActive(2, 0); }); @@ -670,4 +675,92 @@ describe('typeahead tests', function () { }); }); + describe('focus first', function () { + it('should focus the first element by default', function () { + var element = prepareInputEl('
'); + changeInputValueTo(element, 'b'); + expect(element).toBeOpenWithActive(2, 0); + + // Down arrow key + triggerKeyDown(element, 40); + expect(element).toBeOpenWithActive(2, 1); + + // Down arrow key goes back to first element + triggerKeyDown(element, 40); + expect(element).toBeOpenWithActive(2, 0); + + // Up arrow key goes back to last element + triggerKeyDown(element, 38); + expect(element).toBeOpenWithActive(2, 1); + + // Up arrow key goes back to first element + triggerKeyDown(element, 38); + expect(element).toBeOpenWithActive(2, 0); + }); + + it('should not focus the first element until keys are pressed', function () { + var element = prepareInputEl('
'); + changeInputValueTo(element, 'b'); + expect(element).toBeOpenWithActive(2, -1); + + // Down arrow key goes to first element + triggerKeyDown(element, 40); + expect(element).toBeOpenWithActive(2, 0); + + // Down arrow key goes to second element + triggerKeyDown(element, 40); + expect(element).toBeOpenWithActive(2, 1); + + // Down arrow key goes back to first element + triggerKeyDown(element, 40); + expect(element).toBeOpenWithActive(2, 0); + + // Up arrow key goes back to last element + triggerKeyDown(element, 38); + expect(element).toBeOpenWithActive(2, 1); + + // Up arrow key goes back to first element + triggerKeyDown(element, 38); + expect(element).toBeOpenWithActive(2, 0); + + // New input goes back to no focus + changeInputValueTo(element, 'a'); + changeInputValueTo(element, 'b'); + expect(element).toBeOpenWithActive(2, -1); + + // Up arrow key goes to last element + triggerKeyDown(element, 38); + expect(element).toBeOpenWithActive(2, 1); + }); + }); + + it('should not capture enter or tab until an item is focused', function () { + $scope.select_count = 0; + $scope.onSelect = function ($item, $model, $label) { + $scope.select_count = $scope.select_count + 1; + }; + var element = prepareInputEl('
'); + changeInputValueTo(element, 'b'); + + // enter key should not be captured when nothing is focused + triggerKeyDown(element, 13); + expect($scope.keyDownEvent.isDefaultPrevented()).toBeFalsy(); + expect($scope.select_count).toEqual(0); + + // tab key should not be captured when nothing is focused + triggerKeyDown(element, 9); + expect($scope.keyDownEvent.isDefaultPrevented()).toBeFalsy(); + expect($scope.select_count).toEqual(0); + + // down key should be captured and focus first element + triggerKeyDown(element, 40); + expect($scope.keyDownEvent.isDefaultPrevented()).toBeTruthy(); + expect(element).toBeOpenWithActive(2, 0); + + // enter key should be captured now that something is focused + triggerKeyDown(element, 13); + expect($scope.keyDownEvent.isDefaultPrevented()).toBeTruthy(); + expect($scope.select_count).toEqual(1); + }); + }); diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 6bd4f14446..8b080d615f 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -59,6 +59,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; + //INTERNAL VARIABLES //model setter executed upon match selection @@ -131,7 +133,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap if (onCurrentRequest && hasFocus) { if (matches.length > 0) { - scope.activeIdx = 0; + scope.activeIdx = focusFirst ? 0 : -1; scope.matches.length = 0; //transform labels @@ -272,6 +274,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap return; } + // if there's nothing selected (i.e. focusFirst) and enter is hit, don't do anything + if (scope.activeIdx == -1 && (evt.which === 13 || evt.which === 9)) { + return; + } + evt.preventDefault(); if (evt.which === 40) { @@ -279,7 +286,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap scope.$digest(); } else if (evt.which === 38) { - scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; + scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; scope.$digest(); } else if (evt.which === 13 || evt.which === 9) {