Skip to content
This repository was archived by the owner on May 29, 2019. It is now read-only.

Commit 86bfec1

Browse files
davidruswesleycho
authored andcommitted
feat(typeahead): popup position
- Position the dropdown appropriately when appended to the body and window is resized or scrolled Closes #3874
1 parent f6edfa5 commit 86bfec1

File tree

3 files changed

+73
-5
lines changed

3 files changed

+73
-5
lines changed

src/typeahead/test/typeahead.spec.js

+24
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,30 @@ describe('typeahead tests', function () {
752752
changeInputValueTo(element, 'ba');
753753
expect(findDropDown($document.find('body')).length).toEqual(0);
754754
});
755+
756+
it('should have right position after scroll', function() {
757+
var element = prepareInputEl('<div><input ng-model="result" typeahead="item for item in source | filter:$viewValue" typeahead-append-to-body="true"></div>');
758+
var dropdown = findDropDown($document.find('body'));
759+
var body = angular.element(document.body);
760+
761+
// Set body height to allow scrolling
762+
body.css({height:'10000px'});
763+
764+
// Scroll top
765+
window.scroll(0, 1000);
766+
767+
// Set input value to show dropdown
768+
changeInputValueTo(element, 'ba');
769+
770+
// Init position of dropdown must be 1000px
771+
expect(dropdown.css('top') ).toEqual('1000px');
772+
773+
// After scroll, must have new position
774+
window.scroll(0, 500);
775+
body.triggerHandler('scroll');
776+
$timeout.flush();
777+
expect(dropdown.css('top') ).toEqual('500px');
778+
});
755779
});
756780

757781
describe('focus first', function () {

src/typeahead/typeahead.js

+48-4
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
2929
};
3030
}])
3131

32-
.directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$rootScope', '$position', 'typeaheadParser',
33-
function ($compile, $parse, $q, $timeout, $document, $rootScope, $position, typeaheadParser) {
32+
.directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$position', 'typeaheadParser',
33+
function ($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) {
3434

3535
var HOT_KEYS = [9, 13, 27, 38, 40];
36+
var eventDebounceTime = 200;
3637

3738
return {
3839
require:'ngModel',
@@ -96,6 +97,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
9697
matches: 'matches',
9798
active: 'activeIdx',
9899
select: 'select(activeIdx)',
100+
'move-in-progress': 'moveInProgress',
99101
query: 'query',
100102
position: 'position'
101103
});
@@ -153,8 +155,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
153155
//position pop-up with matches - we need to re-calculate its position each time we are opening a window
154156
//with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
155157
//due to other elements being rendered
156-
scope.position = appendToBody ? $position.offset(element) : $position.position(element);
157-
scope.position.top = scope.position.top + element.prop('offsetHeight');
158+
recalculatePosition();
158159

159160
element.attr('aria-expanded', true);
160161
} else {
@@ -170,6 +171,48 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
170171
});
171172
};
172173

174+
// bind events only if appendToBody params exist - performance feature
175+
if (appendToBody) {
176+
angular.element($window).bind('resize', fireRecalculating);
177+
$document.find('body').bind('scroll', fireRecalculating);
178+
}
179+
180+
// Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
181+
var timeoutEventPromise;
182+
183+
// Default progress type
184+
scope.moveInProgress = false;
185+
186+
function fireRecalculating() {
187+
if(!scope.moveInProgress){
188+
scope.moveInProgress = true;
189+
scope.$digest();
190+
}
191+
192+
// Cancel previous timeout
193+
if (timeoutEventPromise) {
194+
$timeout.cancel(timeoutEventPromise);
195+
}
196+
197+
// Debounced executing recalculate after events fired
198+
timeoutEventPromise = $timeout(function () {
199+
// if popup is visible
200+
if (scope.matches.length) {
201+
recalculatePosition();
202+
}
203+
204+
scope.moveInProgress = false;
205+
scope.$digest();
206+
}, eventDebounceTime);
207+
}
208+
209+
// recalculate actual position and set new values to scope
210+
// after digest loop is popup in right position
211+
function recalculatePosition() {
212+
scope.position = appendToBody ? $position.offset(element) : $position.position(element);
213+
scope.position.top += element.prop('offsetHeight');
214+
}
215+
173216
resetMatches();
174217

175218
//we need to propagate user's query so we can higlight matches
@@ -358,6 +401,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
358401
query:'=',
359402
active:'=',
360403
position:'&',
404+
moveInProgress:'=',
361405
select:'&'
362406
},
363407
replace:true,

template/typeahead/typeahead-popup.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<ul class="dropdown-menu" ng-show="isOpen()" ng-style="{top: position().top+'px', left: position().left+'px'}" style="display: block;" role="listbox" aria-hidden="{{!isOpen()}}">
1+
<ul class="dropdown-menu" ng-show="isOpen() && !moveInProgress" ng-style="{top: position().top+'px', left: position().left+'px'}" style="display: block;" role="listbox" aria-hidden="{{!isOpen()}}">
22
<li ng-repeat="match in matches track by $index" ng-class="{active: isActive($index) }" ng-mouseenter="selectActive($index)" ng-click="selectMatch($index)" role="option" id="{{::match.id}}">
33
<div typeahead-match index="$index" match="match" query="query" template-url="templateUrl"></div>
44
</li>

0 commit comments

Comments
 (0)