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

Commit 809ecdb

Browse files
jaydiablowesleycho
authored andcommitted
feat(dropdown): add append-to support
- Adds support for appending dropdown to specified DOM node Closes #4467 Closes #4488
1 parent da455f6 commit 809ecdb

File tree

5 files changed

+148
-19
lines changed

5 files changed

+148
-19
lines changed

src/dropdown/docs/demo.html

+47-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
<li role="menuitem"><a href="#">Separated link</a></li>
5656
</ul>
5757
</div>
58-
58+
5959
<!-- Single button using template-url -->
6060
<div class="btn-group" uib-dropdown>
6161
<button id="button-template-url" type="button" class="btn btn-primary" uib-dropdown-toggle ng-disabled="disabled">
@@ -86,6 +86,52 @@
8686
</ul>
8787
</div>
8888

89+
<hr>
90+
<!-- AppendTo use case -->
91+
<h4>append-to vs. append-to-body vs. inline example</h4>
92+
<div id="dropdown-scrollable-container" style="height: 15em; overflow: auto;">
93+
<div id="dropdown-long-content">
94+
<div id="dropdown-hidden-container">
95+
<div class="btn-group" uib-dropdown dropdown-append-to="appendToEl">
96+
<button id="btn-append-to" type="button" class="btn btn-primary" uib-dropdown-toggle>
97+
Dropdown in Container <span class="caret"></span>
98+
</button>
99+
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="btn-append-to">
100+
<li role="menuitem"><a href="#">Action</a></li>
101+
<li role="menuitem"><a href="#">Another action</a></li>
102+
<li role="menuitem"><a href="#">Something else here</a></li>
103+
<li class="divider"></li>
104+
<li role="menuitem"><a href="#">Separated link</a></li>
105+
</ul>
106+
</div>
107+
<div class="btn-group" uib-dropdown dropdown-append-to-body>
108+
<button id="btn-append-to-to-body" type="button" class="btn btn-primary" uib-dropdown-toggle>
109+
Dropdown on Body <span class="caret"></span>
110+
</button>
111+
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="btn-append-to-to-body">
112+
<li role="menuitem"><a href="#">Action</a></li>
113+
<li role="menuitem"><a href="#">Another action</a></li>
114+
<li role="menuitem"><a href="#">Something else here</a></li>
115+
<li class="divider"></li>
116+
<li role="menuitem"><a href="#">Separated link</a></li>
117+
</ul>
118+
</div>
119+
<div class="btn-group" uib-dropdown>
120+
<button id="btn-append-to-single-button" type="button" class="btn btn-primary" uib-dropdown-toggle>
121+
Inline Dropdown <span class="caret"></span>
122+
</button>
123+
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="btn-append-to-single-button">
124+
<li role="menuitem"><a href="#">Action</a></li>
125+
<li role="menuitem"><a href="#">Another action</a></li>
126+
<li role="menuitem"><a href="#">Something else here</a></li>
127+
<li class="divider"></li>
128+
<li role="menuitem"><a href="#">Separated link</a></li>
129+
</ul>
130+
</div>
131+
</div>
132+
</div>
133+
</div>
134+
89135
<script type="text/ng-template" id="dropdown.html">
90136
<ul class="uib-dropdown-menu" role="menu" aria-labelledby="button-template-url">
91137
<li role="menuitem"><a href="#">Action in Template</a></li>

src/dropdown/docs/demo.js

+2
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ angular.module('ui.bootstrap.demo').controller('DropdownCtrl', function ($scope,
1818
$event.stopPropagation();
1919
$scope.status.isopen = !$scope.status.isopen;
2020
};
21+
22+
$scope.appendToEl = angular.element(document.querySelector('#dropdown-long-content'));
2123
});

src/dropdown/docs/readme.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ There is also the `on-toggle(open)` optional expression fired when dropdown chan
66
Add `dropdown-append-to-body` to the `uib-dropdown` element to append to the inner `dropdown-menu` to the body.
77
This is useful when the dropdown button is inside a div with `overflow: hidden`, and the menu would otherwise be hidden.
88

9+
Pass an [angular.element](https://docs.angularjs.org/api/ng/function/angular.element) object as the `dropdown-append-to` attribute on the `dropdown` element to append the inner `dropdown-menu` to the passed in element. This is particularly useful when appending to the body element isn't possible, perhaps because the dropdown button is enclosed in a scrollable container. Explore the demo on the left to see this in action. Expand the three different dropdowns and try scrolling inside the containing element.
10+
911
Add `uib-keyboard-nav` to the `uib-dropdown` element to enable navigation of dropdown list elements with the arrow keys.
1012

1113
By default the dropdown will automatically close if any of its elements is clicked, you can change this behavior by setting the `auto-close` option as follows:

src/dropdown/dropdown.js

+43-12
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
7474
setIsOpen = angular.noop,
7575
toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
7676
appendToBody = false,
77-
keynavEnabled =false,
78-
selectedOption = null;
79-
77+
appendTo = null,
78+
keynavEnabled = false,
79+
selectedOption = null,
80+
body = $document.find('body');
8081

8182
$element.addClass('dropdown');
8283

@@ -90,11 +91,22 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
9091
});
9192
}
9293

94+
if (angular.isDefined($attrs.dropdownAppendTo)) {
95+
var appendToEl = $parse($attrs.dropdownAppendTo)(scope);
96+
if (appendToEl) {
97+
appendTo = angular.element(appendToEl);
98+
}
99+
}
100+
93101
appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
94102
keynavEnabled = angular.isDefined($attrs.uibKeyboardNav);
95103

96-
if (appendToBody && self.dropdownMenu) {
97-
$document.find('body').append(self.dropdownMenu);
104+
if (appendToBody && !appendTo) {
105+
appendTo = body;
106+
}
107+
108+
if (appendTo && self.dropdownMenu) {
109+
appendTo.append(self.dropdownMenu);
98110
$element.on('$destroy', function handleDestroyEvent() {
99111
self.dropdownMenu.remove();
100112
});
@@ -129,7 +141,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
129141
scope.focusDropdownEntry = function(keyCode) {
130142
var elems = self.dropdownMenu ? //If append to body is used.
131143
(angular.element(self.dropdownMenu).find('a')) :
132-
(angular.element($element).find('ul').eq(0).find('a'));
144+
($element.find('ul').eq(0).find('a'));
133145

134146
switch (keyCode) {
135147
case (40): {
@@ -166,14 +178,17 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
166178
};
167179

168180
scope.$watch('isOpen', function(isOpen, wasOpen) {
169-
if (appendToBody && self.dropdownMenu) {
170-
var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true);
171-
var css = {
181+
if (appendTo && self.dropdownMenu) {
182+
var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true),
183+
css,
184+
rightalign;
185+
186+
css = {
172187
top: pos.top + 'px',
173188
display: isOpen ? 'block' : 'none'
174189
};
175190

176-
var rightalign = self.dropdownMenu.hasClass('dropdown-menu-right');
191+
rightalign = self.dropdownMenu.hasClass('dropdown-menu-right');
177192
if (!rightalign) {
178193
css.left = pos.left + 'px';
179194
css.right = 'auto';
@@ -182,10 +197,26 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
182197
css.right = (window.innerWidth - (pos.left + $element.prop('offsetWidth'))) + 'px';
183198
}
184199

200+
// Need to adjust our positioning to be relative to the appendTo container
201+
// if it's not the body element
202+
if (!appendToBody) {
203+
var appendOffset = $position.offset(appendTo);
204+
205+
css.top = pos.top - appendOffset.top + 'px';
206+
207+
if (!rightalign) {
208+
css.left = pos.left - appendOffset.left + 'px';
209+
} else {
210+
css.right = (window.innerWidth - (pos.left - appendOffset.left + $element.prop('offsetWidth'))) + 'px';
211+
}
212+
}
213+
185214
self.dropdownMenu.css(css);
186215
}
187216

188-
$animate[isOpen ? 'addClass' : 'removeClass']($element, openClass).then(function() {
217+
var openContainer = appendTo ? appendTo : $element;
218+
219+
$animate[isOpen ? 'addClass' : 'removeClass'](openContainer, openClass).then(function() {
189220
if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
190221
toggleInvoker($scope, { open: !!isOpen });
191222
}
@@ -285,7 +316,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
285316
if (!angular.isNumber(dropdownCtrl.selectedOption)) {
286317
dropdownCtrl.selectedOption = 0;
287318
} else {
288-
dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === elems.length -1 ?
319+
dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === elems.length - 1 ?
289320
dropdownCtrl.selectedOption : dropdownCtrl.selectedOption + 1;
290321
}
291322
break;

src/dropdown/test/dropdown.spec.js

+54-6
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,49 @@ describe('dropdownToggle', function() {
232232
});
233233
});
234234

235+
describe('`dropdown-append-to`', function() {
236+
var initialPage;
237+
238+
function dropdown() {
239+
return $compile('<li uib-dropdown dropdown-append-to="appendTo"><a href uib-dropdown-toggle></a><ul class="dropdown-menu" uib-dropdown-menu id="dropdown-menu"><li><a href>Hello On Container</a></li></ul></li>')($rootScope);
240+
}
241+
242+
beforeEach(function() {
243+
$document.find('body').append(angular.element('<div id="dropdown-container"></div>'));
244+
245+
$rootScope.appendTo = $document.find('#dropdown-container');
246+
$rootScope.$digest();
247+
248+
element = dropdown();
249+
$document.find('body').append(element);
250+
});
251+
252+
afterEach(function() {
253+
// Cleanup the extra elements we appended
254+
$document.find('#dropdown-container').remove();
255+
});
256+
257+
it('appends to container', function() {
258+
expect($document.find('#dropdown-menu').parent()[0].id).toBe('dropdown-container');
259+
});
260+
261+
it('toggles open class on container', function() {
262+
var container = $document.find('#dropdown-container');
263+
264+
expect(container.hasClass('open')).toBe(false);
265+
element.find('[uib-dropdown-toggle]').click();
266+
expect(container.hasClass('open')).toBe(true);
267+
element.find('[uib-dropdown-toggle]').click();
268+
expect(container.hasClass('open')).toBe(false);
269+
});
270+
271+
it('removes the menu when the dropdown is removed', function() {
272+
element.remove();
273+
$rootScope.$digest();
274+
expect($document.find('#dropdown-menu').length).toEqual(0);
275+
});
276+
});
277+
235278
describe('integration with $location URL rewriting', function() {
236279
function dropdown() {
237280
// Simulate URL rewriting behavior
@@ -437,11 +480,12 @@ describe('dropdownToggle', function() {
437480
it('should work with dropdown-append-to-body', function() {
438481
element = $compile('<li uib-dropdown dropdown-append-to-body auto-close="outsideClick"><a href uib-dropdown-toggle></a><ul class="uib-dropdown-menu" id="dropdown-menu"><li><a href>Hello On Body</a></li></ul></li>')($rootScope);
439482
clickDropdownToggle();
440-
expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
441-
$document.find('#dropdown-menu').find('li').eq(0).trigger('click');
442-
expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
483+
var dropdownMenu = $document.find('#dropdown-menu');
484+
expect(dropdownMenu.parent().hasClass(dropdownConfig.openClass)).toBe(true);
485+
dropdownMenu.find('li').eq(0).trigger('click');
486+
expect(dropdownMenu.parent().hasClass(dropdownConfig.openClass)).toBe(true);
443487
$document.click();
444-
expect(element.hasClass(dropdownConfig.openClass)).toBe(false);
488+
expect(dropdownMenu.parent().hasClass(dropdownConfig.openClass)).toBe(false);
445489
});
446490
});
447491

@@ -667,7 +711,9 @@ describe('dropdownToggle', function() {
667711

668712
triggerKeyDown(element, 40);
669713

670-
expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
714+
var dropdownMenu = $document.find('#dropdown-menu');
715+
716+
expect(dropdownMenu.parent().hasClass(dropdownConfig.openClass)).toBe(true);
671717
var focusEl = $document.find('ul').eq(0).find('a');
672718
expect(isFocused(focusEl)).toBe(true);
673719
});
@@ -677,7 +723,9 @@ describe('dropdownToggle', function() {
677723
triggerKeyDown(element, 40);
678724
triggerKeyDown(element, 40);
679725

680-
expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
726+
var dropdownMenu = $document.find('#dropdown-menu');
727+
728+
expect(dropdownMenu.parent().hasClass(dropdownConfig.openClass)).toBe(true);
681729
var elem1 = $document.find('ul');
682730
var elem2 = elem1.find('a');
683731
var focusEl = $document.find('ul').eq(0).find('a').eq(1);

0 commit comments

Comments
 (0)