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

Add Dropdown AppendTo functionality #4488

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" dropdown>
<button id="button-template-url" type="button" class="btn btn-primary" 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" style="height: 100em;">
<div id="dropdown-hidden-container" style="height: 4em; overflow: hidden;">
<div class="btn-group" dropdown dropdown-append-to="appendToEl">
<button id="btn-append-to" type="button" class="btn btn-primary" dropdown-toggle>
Dropdown in Container <span class="caret"></span>
</button>
<ul class="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" dropdown dropdown-append-to-body>
<button id="btn-append-to-to-body" type="button" class="btn btn-primary" dropdown-toggle>
Dropdown on Body <span class="caret"></span>
</button>
<ul class="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" dropdown>
<button id="btn-append-to-single-button" type="button" class="btn btn-primary" dropdown-toggle>
Inline Dropdown <span class="caret"></span>
</button>
<ul class="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="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 `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 `keyboard-nav` to the `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
45 changes: 37 additions & 8 deletions src/dropdown/dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
setIsOpen = angular.noop,
toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
appendToBody = false,
appendTo = null,
keynavEnabled = false,
selectedOption = null,
body = $document.find('body');
Expand All @@ -90,12 +91,23 @@ 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.keyboardNav);

if (appendToBody && self.dropdownMenu) {
body.append(self.dropdownMenu);
body.addClass('dropdown');
if (appendToBody && !appendTo) {
appendTo = body;
}

if (appendTo && self.dropdownMenu) {
appendTo.append(self.dropdownMenu);
appendTo.addClass('dropdown');
element.on('$destroy', function handleDestroyEvent() {
self.dropdownMenu.remove();
});
Expand Down Expand Up @@ -167,14 +179,17 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
};

scope.$watch('isOpen', function(isOpen, wasOpen) {
if (appendToBody && self.dropdownMenu) {
var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true);
var css = {
if (appendTo && self.dropdownMenu) {
var pos = $position.positionElements(self.$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 @@ -183,10 +198,24 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
css.right = (window.innerWidth - (pos.left + self.$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 + self.$element.prop('offsetWidth'))) + 'px';
}
}

self.dropdownMenu.css(css);
}

var openContainer = appendToBody ? body : self.$element;
var openContainer = appendTo ? appendTo : self.$element;

$animate[isOpen ? 'addClass' : 'removeClass'](openContainer, openClass).then(function() {
if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
Expand Down
51 changes: 51 additions & 0 deletions src/dropdown/test/dropdown.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ describe('dropdownToggle', function() {
element = dropdown();
});

afterEach(function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This afterEach is unnecessary once this is rebased off of current master.

$document.find('body').removeClass('dropdown');
});

it('adds the menu to the body', function() {
expect($document.find('#dropdown-menu').parent()[0]).toBe($document.find('body')[0]);
});
Expand Down Expand Up @@ -249,6 +253,53 @@ describe('dropdownToggle', function() {
});
});

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

function dropdown() {
return $compile('<li dropdown dropdown-append-to="appendTo"><a href dropdown-toggle></a><ul class="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('adds dropdown class to container', function() {
expect($document.find('#dropdown-container').hasClass('dropdown')).toBe(true);
});

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

expect(container.hasClass('open')).toBe(false);
element.find('[dropdown-toggle]').click();
expect(container.hasClass('open')).toBe(true);
element.find('[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