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

Commit

Permalink
feat(dropdown): add append-to support
Browse files Browse the repository at this point in the history
- Adds support for appending dropdown to specified DOM node

Closes #4467
Closes #4488
  • Loading branch information
jaydiablo authored and wesleycho committed Oct 31, 2015
1 parent da455f6 commit 809ecdb
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 19 deletions.
48 changes: 47 additions & 1 deletion src/dropdown/docs/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
<li role="menuitem"><a href="#">Separated link</a></li>
</ul>
</div>

<!-- Single button using template-url -->
<div class="btn-group" uib-dropdown>
<button id="button-template-url" type="button" class="btn btn-primary" uib-dropdown-toggle ng-disabled="disabled">
Expand Down Expand Up @@ -86,6 +86,52 @@
</ul>
</div>

<hr>
<!-- AppendTo use case -->
<h4>append-to vs. append-to-body vs. inline example</h4>
<div id="dropdown-scrollable-container" style="height: 15em; overflow: auto;">
<div id="dropdown-long-content">
<div id="dropdown-hidden-container">
<div class="btn-group" uib-dropdown dropdown-append-to="appendToEl">
<button id="btn-append-to" type="button" class="btn btn-primary" uib-dropdown-toggle>
Dropdown in Container <span class="caret"></span>
</button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="btn-append-to">
<li role="menuitem"><a href="#">Action</a></li>
<li role="menuitem"><a href="#">Another action</a></li>
<li role="menuitem"><a href="#">Something else here</a></li>
<li class="divider"></li>
<li role="menuitem"><a href="#">Separated link</a></li>
</ul>
</div>
<div class="btn-group" uib-dropdown dropdown-append-to-body>
<button id="btn-append-to-to-body" type="button" class="btn btn-primary" uib-dropdown-toggle>
Dropdown on Body <span class="caret"></span>
</button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="btn-append-to-to-body">
<li role="menuitem"><a href="#">Action</a></li>
<li role="menuitem"><a href="#">Another action</a></li>
<li role="menuitem"><a href="#">Something else here</a></li>
<li class="divider"></li>
<li role="menuitem"><a href="#">Separated link</a></li>
</ul>
</div>
<div class="btn-group" uib-dropdown>
<button id="btn-append-to-single-button" type="button" class="btn btn-primary" uib-dropdown-toggle>
Inline Dropdown <span class="caret"></span>
</button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="btn-append-to-single-button">
<li role="menuitem"><a href="#">Action</a></li>
<li role="menuitem"><a href="#">Another action</a></li>
<li role="menuitem"><a href="#">Something else here</a></li>
<li class="divider"></li>
<li role="menuitem"><a href="#">Separated link</a></li>
</ul>
</div>
</div>
</div>
</div>

<script type="text/ng-template" id="dropdown.html">
<ul class="uib-dropdown-menu" role="menu" aria-labelledby="button-template-url">
<li role="menuitem"><a href="#">Action in Template</a></li>
Expand Down
2 changes: 2 additions & 0 deletions src/dropdown/docs/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ angular.module('ui.bootstrap.demo').controller('DropdownCtrl', function ($scope,
$event.stopPropagation();
$scope.status.isopen = !$scope.status.isopen;
};

$scope.appendToEl = angular.element(document.querySelector('#dropdown-long-content'));
});
2 changes: 2 additions & 0 deletions src/dropdown/docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ There is also the `on-toggle(open)` optional expression fired when dropdown chan
Add `dropdown-append-to-body` to the `uib-dropdown` element to append to the inner `dropdown-menu` to the body.
This is useful when the dropdown button is inside a div with `overflow: hidden`, and the menu would otherwise be hidden.

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.

Add `uib-keyboard-nav` to the `uib-dropdown` element to enable navigation of dropdown list elements with the arrow keys.

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:
Expand Down
55 changes: 43 additions & 12 deletions src/dropdown/dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
setIsOpen = angular.noop,
toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
appendToBody = false,
keynavEnabled =false,
selectedOption = null;

appendTo = null,
keynavEnabled = false,
selectedOption = null,
body = $document.find('body');

$element.addClass('dropdown');

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

if (angular.isDefined($attrs.dropdownAppendTo)) {
var appendToEl = $parse($attrs.dropdownAppendTo)(scope);
if (appendToEl) {
appendTo = angular.element(appendToEl);
}
}

appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
keynavEnabled = angular.isDefined($attrs.uibKeyboardNav);

if (appendToBody && self.dropdownMenu) {
$document.find('body').append(self.dropdownMenu);
if (appendToBody && !appendTo) {
appendTo = body;
}

if (appendTo && self.dropdownMenu) {
appendTo.append(self.dropdownMenu);
$element.on('$destroy', function handleDestroyEvent() {
self.dropdownMenu.remove();
});
Expand Down Expand Up @@ -129,7 +141,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
scope.focusDropdownEntry = function(keyCode) {
var elems = self.dropdownMenu ? //If append to body is used.
(angular.element(self.dropdownMenu).find('a')) :
(angular.element($element).find('ul').eq(0).find('a'));
($element.find('ul').eq(0).find('a'));

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

scope.$watch('isOpen', function(isOpen, wasOpen) {
if (appendToBody && self.dropdownMenu) {
var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true);
var css = {
if (appendTo && self.dropdownMenu) {
var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true),
css,
rightalign;

css = {
top: pos.top + 'px',
display: isOpen ? 'block' : 'none'
};

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

// Need to adjust our positioning to be relative to the appendTo container
// if it's not the body element
if (!appendToBody) {
var appendOffset = $position.offset(appendTo);

css.top = pos.top - appendOffset.top + 'px';

if (!rightalign) {
css.left = pos.left - appendOffset.left + 'px';
} else {
css.right = (window.innerWidth - (pos.left - appendOffset.left + $element.prop('offsetWidth'))) + 'px';
}
}

self.dropdownMenu.css(css);
}

$animate[isOpen ? 'addClass' : 'removeClass']($element, openClass).then(function() {
var openContainer = appendTo ? appendTo : $element;

$animate[isOpen ? 'addClass' : 'removeClass'](openContainer, openClass).then(function() {
if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
toggleInvoker($scope, { open: !!isOpen });
}
Expand Down Expand Up @@ -285,7 +316,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
if (!angular.isNumber(dropdownCtrl.selectedOption)) {
dropdownCtrl.selectedOption = 0;
} else {
dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === elems.length -1 ?
dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === elems.length - 1 ?
dropdownCtrl.selectedOption : dropdownCtrl.selectedOption + 1;
}
break;
Expand Down
60 changes: 54 additions & 6 deletions src/dropdown/test/dropdown.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,49 @@ describe('dropdownToggle', function() {
});
});

describe('`dropdown-append-to`', function() {
var initialPage;

function dropdown() {
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);
}

beforeEach(function() {
$document.find('body').append(angular.element('<div id="dropdown-container"></div>'));

$rootScope.appendTo = $document.find('#dropdown-container');
$rootScope.$digest();

element = dropdown();
$document.find('body').append(element);
});

afterEach(function() {
// Cleanup the extra elements we appended
$document.find('#dropdown-container').remove();
});

it('appends to container', function() {
expect($document.find('#dropdown-menu').parent()[0].id).toBe('dropdown-container');
});

it('toggles open class on container', function() {
var container = $document.find('#dropdown-container');

expect(container.hasClass('open')).toBe(false);
element.find('[uib-dropdown-toggle]').click();
expect(container.hasClass('open')).toBe(true);
element.find('[uib-dropdown-toggle]').click();
expect(container.hasClass('open')).toBe(false);
});

it('removes the menu when the dropdown is removed', function() {
element.remove();
$rootScope.$digest();
expect($document.find('#dropdown-menu').length).toEqual(0);
});
});

describe('integration with $location URL rewriting', function() {
function dropdown() {
// Simulate URL rewriting behavior
Expand Down Expand Up @@ -437,11 +480,12 @@ describe('dropdownToggle', function() {
it('should work with dropdown-append-to-body', function() {
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);
clickDropdownToggle();
expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
$document.find('#dropdown-menu').find('li').eq(0).trigger('click');
expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
var dropdownMenu = $document.find('#dropdown-menu');
expect(dropdownMenu.parent().hasClass(dropdownConfig.openClass)).toBe(true);
dropdownMenu.find('li').eq(0).trigger('click');
expect(dropdownMenu.parent().hasClass(dropdownConfig.openClass)).toBe(true);
$document.click();
expect(element.hasClass(dropdownConfig.openClass)).toBe(false);
expect(dropdownMenu.parent().hasClass(dropdownConfig.openClass)).toBe(false);
});
});

Expand Down Expand Up @@ -667,7 +711,9 @@ describe('dropdownToggle', function() {

triggerKeyDown(element, 40);

expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
var dropdownMenu = $document.find('#dropdown-menu');

expect(dropdownMenu.parent().hasClass(dropdownConfig.openClass)).toBe(true);
var focusEl = $document.find('ul').eq(0).find('a');
expect(isFocused(focusEl)).toBe(true);
});
Expand All @@ -677,7 +723,9 @@ describe('dropdownToggle', function() {
triggerKeyDown(element, 40);
triggerKeyDown(element, 40);

expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
var dropdownMenu = $document.find('#dropdown-menu');

expect(dropdownMenu.parent().hasClass(dropdownConfig.openClass)).toBe(true);
var elem1 = $document.find('ul');
var elem2 = elem1.find('a');
var focusEl = $document.find('ul').eq(0).find('a').eq(1);
Expand Down

0 comments on commit 809ecdb

Please sign in to comment.