Skip to content

Commit

Permalink
feat(dropdown): distinguish inside and outside clicks for autoClose
Browse files Browse the repository at this point in the history
Closes #1022

BREAKING CHANGE:

Dropdown menu now requires usage of the new `ngbDropdownMenu` directive.

Before:

```html
<div ngbDropdown>
  <button ngbDropdownToggle>Toggle dropdown</button>
  <div class="dropdown-menu">
    <a class="dropdown-item">Action</a>
  </div>
</div>
```

After (notice **ngbDropdownMenu**):

```html
<div ngbDropdown>
  <button ngbDropdownToggle>Toggle dropdown</button>
  <div ngbDropdownMenu>
    <a class="dropdown-item">Action</a>
  </div>
</div>
```
  • Loading branch information
pkozlowski-opensource committed Aug 11, 2017
1 parent cf5ea47 commit bb975af
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="col">
<div ngbDropdown class="d-inline-block">
<button class="btn btn-outline-primary" id="dropdownBasic1" ngbDropdownToggle>Toggle dropdown</button>
<div class="dropdown-menu" aria-labelledby="dropdownBasic1">
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<button class="dropdown-item">Action - 1</button>
<button class="dropdown-item">Another Action</button>
<button class="dropdown-item">Something else is here</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<div ngbDropdown>
<button class="btn btn-outline-primary" id="dropdownConfig" ngbDropdownToggle>Toggle</button>
<div class="dropdown-menu" aria-labelledby="dropdownConfig">
<div ngbDropdownMenu aria-labelledby="dropdownConfig">
<button class="dropdown-item">Action - 1</button>
<button class="dropdown-item">Another Action</button>
<button class="dropdown-item">Something else is here</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<div class="d-inline-block" ngbDropdown #myDrop="ngbDropdown">
<button class="btn btn-outline-primary" id="dropdownManual" ngbDropdownToggle>Toggle dropdown</button>
<div class="dropdown-menu" aria-labelledby="dropdownManual">
<div ngbDropdownMenu aria-labelledby="dropdownManual">
<button class="dropdown-item">Action - 1</button>
<button class="dropdown-item">Another Action</button>
<button class="dropdown-item">Something else is here</button>
Expand Down
6 changes: 3 additions & 3 deletions src/dropdown/dropdown.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {NgModule, ModuleWithProviders} from '@angular/core';
import {NgbDropdown, NgbDropdownToggle} from './dropdown';
import {NgbDropdown, NgbDropdownToggle, NgbDropdownMenu} from './dropdown';
import {NgbDropdownConfig} from './dropdown-config';

export {NgbDropdown, NgbDropdownToggle} from './dropdown';
export {NgbDropdown, NgbDropdownToggle, NgbDropdownMenu} from './dropdown';
export {NgbDropdownConfig} from './dropdown-config';

const NGB_DROPDOWN_DIRECTIVES = [NgbDropdownToggle, NgbDropdown];
const NGB_DROPDOWN_DIRECTIVES = [NgbDropdown, NgbDropdownToggle, NgbDropdownMenu];

@NgModule({declarations: NGB_DROPDOWN_DIRECTIVES, exports: NGB_DROPDOWN_DIRECTIVES})
export class NgbDropdownModule {
Expand Down
77 changes: 71 additions & 6 deletions src/dropdown/dropdown.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,11 +308,11 @@ describe('ngb-dropdown-toggle', () => {
expect(dropdownEl).toHaveCssClass('show');
});

it('should close on item click if autoClose is set to false', () => {
it('should not close on item click if autoClose is set to false', () => {
const html = `
<div ngbDropdown [open]="true" [autoClose]="false">
<button ngbDropdownToggle>Toggle dropdown</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenu1">
<div ngbDropdownMenu>
<a class="dropdown-item">Action</a>
</div>
</div>`;
Expand All @@ -330,11 +330,11 @@ describe('ngb-dropdown-toggle', () => {
expect(dropdownEl).toHaveCssClass('show');
});

it('should close on item click', () => {
it('should close on item click by default', () => {
const html = `
<div ngbDropdown [open]="true">
<button ngbDropdownToggle>Toggle dropdown</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenu1">
<div ngbDropdownMenu aria-labelledby="dropdownMenu1">
<a class="dropdown-item">Action</a>
</div>
</div>`;
Expand All @@ -357,13 +357,13 @@ describe('ngb-dropdown-toggle', () => {
const html = `
<div ngbDropdown>
<button ngbDropdownToggle>Toggle dropdown 1</button>
<div class="dropdown-menu">
<div ngbDropdownMenu>
<a class="dropdown-item">Action 1</a>
</div>
</div>
<div ngbDropdown>
<button ngbDropdownToggle>Toggle dropdown 2</button>
<div class="dropdown-menu">
<div ngbDropdownMenu>
<a class="dropdown-item">Action 2</a>
</div>
</div>`;
Expand All @@ -389,6 +389,71 @@ describe('ngb-dropdown-toggle', () => {
expect(dropdownEls[1]).toHaveCssClass('show');
});

describe('outside and inside clicks', () => {

it('should not close on menu clicks when the "outside" option is used', () => {

const html = `
<div ngbDropdown [open]="true" autoClose="outside">
<button ngbDropdownToggle>Toggle dropdown</button>
<div ngbDropdownMenu>
<a class="dropdown-item">Action</a>
</div>
</div>`;

const fixture = createTestComponent(html);
const compiled = fixture.nativeElement;
const dropdownEl = getDropdownEl(compiled);
const buttonEl = compiled.querySelector('button');
let linkEl = compiled.querySelector('a');

fixture.detectChanges();
expect(dropdownEl).toHaveCssClass('show');

// remains open on item click
linkEl.click();
fixture.detectChanges();
expect(dropdownEl).toHaveCssClass('show');

// but closes on toggle button click
buttonEl.click();
fixture.detectChanges();
expect(dropdownEl).not.toHaveCssClass('show');
});

it('should not close on outside clicks when the "inside" option is used', () => {

const html = `
<button id="outside">Outside</button>
<div ngbDropdown [open]="true" autoClose="inside">
<button ngbDropdownToggle>Toggle dropdown</button>
<div ngbDropdownMenu>
<a class="dropdown-item">Action</a>
</div>
</div>`;

const fixture = createTestComponent(html);
const compiled = fixture.nativeElement;
const dropdownEl = getDropdownEl(compiled);
const buttonEl = compiled.querySelector('#outside');
let linkEl = compiled.querySelector('a');

fixture.detectChanges();
expect(dropdownEl).toHaveCssClass('show');

// remains open on outside click
buttonEl.click();
fixture.detectChanges();
expect(dropdownEl).toHaveCssClass('show');

// but closes on item click
linkEl.click();
fixture.detectChanges();
expect(dropdownEl).not.toHaveCssClass('show');
});

});

describe('Custom config', () => {
let config: NgbDropdownConfig;

Expand Down
84 changes: 55 additions & 29 deletions src/dropdown/dropdown.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
import {Directive, Input, Output, EventEmitter, ElementRef} from '@angular/core';
import {forwardRef, Inject, Directive, Input, Output, EventEmitter, ElementRef, ContentChild} from '@angular/core';
import {NgbDropdownConfig} from './dropdown-config';

/**
*/
@Directive({selector: '[ngbDropdownMenu]', host: {'[class.dropdown-menu]': 'true'}})
export class NgbDropdownMenu {
constructor(private _elementRef: ElementRef) {}

isEventFrom($event) { return this._elementRef.nativeElement.contains($event.target); }
}

/**
* Allows the dropdown to be toggled via click. This directive is optional.
*/
@Directive({
selector: '[ngbDropdownToggle]',
host: {
'class': 'dropdown-toggle',
'aria-haspopup': 'true',
'[attr.aria-expanded]': 'dropdown.isOpen()',
'(click)': 'toggleOpen()'
}
})
export class NgbDropdownToggle {
constructor(@Inject(forwardRef(() => NgbDropdown)) public dropdown, private _elementRef: ElementRef) {}

toggleOpen() { this.dropdown.toggle(); }

isEventFrom($event) { return this._elementRef.nativeElement.contains($event.target); }
}

/**
* Transforms a node into a dropdown.
*/
Expand All @@ -12,11 +41,13 @@ import {NgbDropdownConfig} from './dropdown-config';
'[class.dropup]': 'up',
'[class.show]': 'isOpen()',
'(keyup.esc)': 'closeFromOutsideEsc()',
'(document:click)': 'closeFromOutsideClick($event)'
'(document:click)': 'closeFromClick($event)'
}
})
export class NgbDropdown {
private _toggleElement: any;
@ContentChild(NgbDropdownMenu) private _menu: NgbDropdownMenu;

@ContentChild(NgbDropdownToggle) private _toggle: NgbDropdownToggle;

/**
* Indicates that the dropdown should open upwards
Expand All @@ -26,7 +57,7 @@ export class NgbDropdown {
/**
* Indicates that dropdown should be closed when selecting one of dropdown items (click) or pressing ESC.
*/
@Input() autoClose: boolean;
@Input() autoClose: boolean | 'outside' | 'inside';

/**
* Defines whether or not the dropdown-menu is open initially.
Expand Down Expand Up @@ -81,9 +112,15 @@ export class NgbDropdown {
}
}

closeFromOutsideClick($event) {
closeFromClick($event) {
if (this.autoClose && $event.button !== 2 && !this._isEventFromToggle($event)) {
this.close();
if (this.autoClose === true) {
this.close();
} else if (this.autoClose === 'inside' && this._isEventFromMenu($event)) {
this.close();
} else if (this.autoClose === 'outside' && !this._isEventFromMenu($event)) {
this.close();
}
}
}

Expand All @@ -93,30 +130,19 @@ export class NgbDropdown {
}
}

/**
* @internal
*/
set toggleElement(toggleElement: any) { this._toggleElement = toggleElement; }

private _isEventFromToggle($event) { return !!this._toggleElement && this._toggleElement.contains($event.target); }
}
private _isEventFromToggle($event) {
if (this._toggle) {
return this._toggle.isEventFrom($event);
}

/**
* Allows the dropdown to be toggled via click. This directive is optional.
*/
@Directive({
selector: '[ngbDropdownToggle]',
host: {
'class': 'dropdown-toggle',
'aria-haspopup': 'true',
'[attr.aria-expanded]': 'dropdown.isOpen()',
'(click)': 'toggleOpen()'
}
})
export class NgbDropdownToggle {
constructor(public dropdown: NgbDropdown, elementRef: ElementRef) {
dropdown.toggleElement = elementRef.nativeElement;
return false;
}

toggleOpen() { this.dropdown.toggle(); }
private _isEventFromMenu($event) {
if (this._menu) {
return this._menu.isEventFrom($event);
}

return false;
}
}

0 comments on commit bb975af

Please sign in to comment.