Skip to content

Commit

Permalink
fix(dropdown): close dropdown only on dropdown elements click
Browse files Browse the repository at this point in the history
Fixes #3063
Fixes #3143
  • Loading branch information
fbasso authored and maxokorokov committed May 6, 2019
1 parent c41da80 commit fa8e45f
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ <h3>
<div class="form-group form-inline">
<div id="dropdown" #d="ngbDropdown" ngbDropdown class="d-inline-block" [autoClose]="autoClose" [container]="container">
<button class="btn btn-outline-secondary" ngbDropdownToggle>Dropdown</button>
<div id="dropdownMenuId" ngbDropdownMenu aria-labelledby="dropdownBasic1">
<div id="dropdownMenuId" ngbDropdownMenu aria-labelledby="dropdownBasic1" style="overflow-y: scroll;">
<div class="form-group">
<input type="email" class="form-control" id="emailInput" placeholder="email@example.com">
</div>
<button class="dropdown-item">Item</button>
</div>
</div>
Expand Down
34 changes: 31 additions & 3 deletions e2e-app/src/app/dropdown/autoclose/dropdown-autoclose.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {Key, ElementFinder} from 'protractor';
import {sendKey, openUrl, rightClick} from '../../tools.po';
import {Key, ElementFinder, browser} from 'protractor';
import {sendKey, openUrl, rightClick, offsetClick} from '../../tools.po';
import {DropdownAutoClosePage} from './dropdown-autoclose.po';
import {DropdownPage} from '../dropdown.po';

const containers = [null, 'body'];
containers.forEach((container) => {
describe('Dropdown Autoclose with container = ' + container, () => {
let page: DropdownAutoClosePage;
let dropdownPage: DropdownPage;

const expectDropdownToBeVisible = async(dropdown: ElementFinder, message: string) => {
expect(await page.isOpened(dropdown)).toBeTruthy(message);
Expand All @@ -23,7 +24,10 @@ containers.forEach((container) => {
await expectDropdownToBeVisible(dropdown, message);
};

beforeAll(() => page = new DropdownAutoClosePage());
beforeAll(() => {
page = new DropdownAutoClosePage();
dropdownPage = new DropdownPage();
});

beforeEach(async() => await openUrl('dropdown/autoclose'));

Expand Down Expand Up @@ -59,6 +63,30 @@ containers.forEach((container) => {
await expectDropdownToBeHidden(dropdown, `Dropdown should be closed on inside click`);
});

it(`should't close when clicking on the form input`, async() => {
await page.selectAutoClose('true');
const dropdown = page.getDropdown('#dropdown');

await openDropdown(dropdown, `Opening dropdown`);
await page.getFormInput(dropdown).click();
await expectDropdownToBeVisible(dropdown, `Dropdown should be open`);

});

it(`should't close when clicking on the scrollbar`, async() => {
await page.selectAutoClose('true');
const dropdown = page.getDropdown('#dropdown');

await openDropdown(dropdown, `Opening dropdown`);

const dropdownMenu = dropdownPage.getDropdownMenu('#dropdownMenuId');
const dropdownSize = await dropdownMenu.getSize();

offsetClick(dropdownMenu, {x: dropdownSize.width - 5, y: dropdownSize.height / 2});
await expectDropdownToBeVisible(dropdown, `Dropdown should be open`);

});

it(`should work when autoClose === false`, async() => {
await page.selectAutoClose('false');
const dropdown = page.getDropdown('#dropdown');
Expand Down
2 changes: 2 additions & 0 deletions e2e-app/src/app/dropdown/autoclose/dropdown-autoclose.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export class DropdownAutoClosePage {

getDropdown(dropDownSelector = '') { return $(`${dropDownSelector}[ngbDropdown]`); }

getFormInput(dropdown: ElementFinder) { return dropdown.$(`input`); }

getFirstItem(dropdown: ElementFinder) { return dropdown.$$(`.dropdown-item`).first(); }

getOpenStatus() { return $('#open-status'); }
Expand Down
9 changes: 9 additions & 0 deletions e2e-app/src/app/tools.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ export const rightClick = async(el: ElementFinder) => {
await browser.actions().click(el, Button.RIGHT).perform();
};

/**
* Clicks on the given element with a given offset.
* @param el element to right click on
* @param offset {x, y} position, relative to the element
*/
export const offsetClick = async(el: ElementFinder, offset) => {
await browser.actions().mouseMove(el, offset).click().perform();
};

/**
* Expects provided element to be focused
*
Expand Down
3 changes: 2 additions & 1 deletion src/dropdown/dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ export class NgbDropdown implements OnInit, OnDestroy {
private _setCloseHandlers() {
ngbAutoClose(
this._ngZone, this._document, this.autoClose, () => this.close(), this._closed$,
this._menu ? [this._menuElement.nativeElement] : [], this._anchor ? [this._anchor.getNativeElement()] : []);
this._menu ? [this._menuElement.nativeElement] : [], this._anchor ? [this._anchor.getNativeElement()] : [],
'.dropdown-item,.dropdown-divider');
}

/**
Expand Down
9 changes: 6 additions & 3 deletions src/util/autoclose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {NgZone} from '@angular/core';
import {fromEvent, Observable, race} from 'rxjs';
import {delay, filter, map, takeUntil, withLatestFrom} from 'rxjs/operators';
import {Key} from './key';
import {closest} from './util';

const isHTMLElementContainedIn = (element: HTMLElement, array?: HTMLElement[]) =>
array ? array.some(item => item.contains(element)) : false;
Expand All @@ -16,7 +17,7 @@ if (typeof navigator !== 'undefined') {

export function ngbAutoClose(
zone: NgZone, document: any, type: boolean | 'inside' | 'outside', close: () => void, closed$: Observable<any>,
insideElements: HTMLElement[], ignoreElements?: HTMLElement[]) {
insideElements: HTMLElement[], ignoreElements?: HTMLElement[], insideSelector?: string) {
// closing on ESC and outside clicks
if (type) {
zone.runOutsideAngular(() => {
Expand All @@ -27,11 +28,13 @@ export function ngbAutoClose(
return false;
}
if (type === 'inside') {
return isHTMLElementContainedIn(element, insideElements);
return isHTMLElementContainedIn(element, insideElements) &&
(!insideSelector || closest(element, insideSelector) != null);
} else if (type === 'outside') {
return !isHTMLElementContainedIn(element, insideElements);
} else /* if (type === true) */ {
return true;
return !insideSelector || !isHTMLElementContainedIn(element, insideElements) ||
closest(element, insideSelector) != null;
}
};

Expand Down
31 changes: 31 additions & 0 deletions src/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,34 @@ export function hasClassName(element: any, className: string): boolean {
return element && element.className && element.className.split &&
element.className.split(/\s+/).indexOf(className) >= 0;
}

if (typeof Element !== 'undefined' && !Element.prototype.closest) {
// Polyfill for ie10+

if (!Element.prototype.matches) {
// IE uses the non-standard name: msMatchesSelector
Element.prototype.matches = (Element.prototype as any).msMatchesSelector || Element.prototype.webkitMatchesSelector;
}

Element.prototype.closest = function(s: string) {
let el = this;
if (!document.documentElement.contains(el)) {
return null;
}
do {
if (el.matches(s)) {
return el;
}
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
};
}

export function closest(element: HTMLElement, selector): HTMLElement {
if (!selector) {
return null;
}

return element.closest(selector);
}

0 comments on commit fa8e45f

Please sign in to comment.