Skip to content

Commit

Permalink
feat(dropdown): allow using any event to toggle dropdowns
Browse files Browse the repository at this point in the history
Closes #1115
Fixes #1926

Closes #2082
  • Loading branch information
pkozlowski-opensource committed Mar 30, 2018
1 parent acad88a commit 25e0d39
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<p>You can easily control dropdowns programmatically using the exported dropdown instance.</p>

<div class="d-inline-block" ngbDropdown #myDrop="ngbDropdown">
<button class="btn btn-outline-primary" id="dropdownManual" ngbDropdownToggle>Toggle dropdown</button>
<button class="btn btn-outline-primary" id="dropdownManual" ngbDropdownAnchor (focus)="myDrop.open()">Toggle dropdown</button>
<div ngbDropdownMenu aria-labelledby="dropdownManual">
<button class="dropdown-item">Action - 1</button>
<button class="dropdown-item">Another Action</button>
Expand Down
3 changes: 2 additions & 1 deletion demo/src/app/components/dropdown/dropdown.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import {DEMO_SNIPPETS} from './demos';
<ngbd-component-wrapper component="Dropdown">
<ngbd-api-docs directive="NgbDropdown"></ngbd-api-docs>
<ngbd-api-docs directive="NgbDropdownToggle"></ngbd-api-docs>
<ngbd-api-docs directive="NgbDropdownAnchor"></ngbd-api-docs>
<ngbd-api-docs-config type="NgbDropdownConfig"></ngbd-api-docs-config>
<ngbd-example-box demoTitle="Dropdown" [snippets]="snippets" component="dropdown" demo="basic">
<ngbd-dropdown-basic></ngbd-dropdown-basic>
</ngbd-example-box>
<ngbd-example-box demoTitle="Manual triggers" [snippets]="snippets" component="dropdown" demo="manual">
<ngbd-example-box demoTitle="Manual and custom triggers" [snippets]="snippets" component="dropdown" demo="manual">
<ngbd-dropdown-manual></ngbd-dropdown-manual>
</ngbd-example-box>
<ngbd-example-box demoTitle="Button groups and split buttons" [snippets]="snippets" component="dropdown" demo="split">
Expand Down
4 changes: 2 additions & 2 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, NgbDropdownMenu} from './dropdown';
import {NgbDropdown, NgbDropdownAnchor, NgbDropdownToggle, NgbDropdownMenu} from './dropdown';
import {NgbDropdownConfig} from './dropdown-config';

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

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

@NgModule({declarations: NGB_DROPDOWN_DIRECTIVES, exports: NGB_DROPDOWN_DIRECTIVES})
export class NgbDropdownModule {
Expand Down
30 changes: 26 additions & 4 deletions src/dropdown/dropdown.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe('ngb-dropdown', () => {
it('should be closed and down by default', () => {
const html = `
<div ngbDropdown>
<button ngbDropdownAnchor></button>
<div ngbDropdownMenu>
<a class="dropdown-item">dropDown item</a>
<a class="dropdown-item">dropDown item</a>
Expand All @@ -71,6 +72,7 @@ describe('ngb-dropdown', () => {
it('should have dropup CSS class if placed on top', () => {
const html = `
<div ngbDropdown placement="top">
<button ngbDropdownAnchor></button>
<div ngbDropdownMenu>
<a class="dropdown-item">dropDown item</a>
<a class="dropdown-item">dropDown item</a>
Expand All @@ -86,6 +88,7 @@ describe('ngb-dropdown', () => {
it('should have dropdown CSS class if placement is other than top', () => {
const html = `
<div ngbDropdown placement="bottom">
<button ngbDropdownAnchor></button>
<div ngbDropdownMenu>
<a class="dropdown-item">dropDown item</a>
<a class="dropdown-item">dropDown item</a>
Expand All @@ -101,6 +104,7 @@ describe('ngb-dropdown', () => {
it('should be open initially if open expression is true', () => {
const html = `
<div ngbDropdown [open]="true">
<button ngbDropdownAnchor></button>
<div ngbDropdownMenu>
<a class="dropdown-item">dropDown item</a>
<a class="dropdown-item">dropDown item</a>
Expand All @@ -114,7 +118,11 @@ describe('ngb-dropdown', () => {
});

it('should toggle open on "open" binding change', () => {
const html = `<div ngbDropdown [open]="isOpen"><div ngbDropdownMenu></div></div>`;
const html = `
<div ngbDropdown [open]="isOpen">
<button ngbDropdownAnchor></button>
<div ngbDropdownMenu></div>
</div>`;

const fixture = createTestComponent(html);
const compiled = fixture.nativeElement;
Expand All @@ -135,7 +143,10 @@ describe('ngb-dropdown', () => {
<button (click)="drop.open(); $event.stopPropagation()">Open</button>
<button (click)="drop.close(); $event.stopPropagation()">Close</button>
<button (click)="drop.toggle(); $event.stopPropagation()">Toggle</button>
<div ngbDropdown #drop="ngbDropdown"><div ngbDropdownMenu></div></div>`;
<div ngbDropdown #drop="ngbDropdown">
<button ngbDropdownAnchor></button>
<div ngbDropdownMenu></div>
</div>`;

const fixture = createTestComponent(html);
const compiled = fixture.nativeElement;
Expand Down Expand Up @@ -273,7 +284,12 @@ describe('ngb-dropdown-toggle', () => {
});

it('should close on outside click', () => {
const html = `<button>Outside</button><div ngbDropdown [open]="true"><div ngbDropdownMenu></div></div>`;
const html = `
<button>Outside</button>
<div ngbDropdown [open]="true">
<button ngbDropdownAnchor></button>
<div ngbDropdownMenu></div>
</div>`;

const fixture = createTestComponent(html);
const compiled = fixture.nativeElement;
Expand All @@ -288,7 +304,12 @@ describe('ngb-dropdown-toggle', () => {
});

it('should not close on outside click if right button click', () => {
const html = `<button>Outside</button><div ngbDropdown [open]="true"><div ngbDropdownMenu></div></div>`;
const html = `
<button>Outside</button>
<div ngbDropdown [open]="true">
<button ngbDropdownAnchor></button>
<div ngbDropdownMenu></div>
</div>`;

const fixture = createTestComponent(html);
const compiled = fixture.nativeElement;
Expand All @@ -308,6 +329,7 @@ describe('ngb-dropdown-toggle', () => {
const html = `
<button>Outside</button>
<div ngbDropdown [open]="true" [autoClose]="false">
<button ngbDropdownAnchor></button>
<div ngbDropdownMenu></div>
</div>`;

Expand Down
44 changes: 30 additions & 14 deletions src/dropdown/dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,28 @@ export class NgbDropdownMenu {
}

/**
* Allows the dropdown to be toggled via click. This directive is optional.
* Marks an element to which dropdown menu will be anchored. This is a simple version
* of the NgbDropdownToggle directive. It plays the same role as NgbDropdownToggle but
* doesn't listen to click events to toggle dropdown menu thus enabling support for
* events other than click.
*/
@Directive({
selector: '[ngbDropdownAnchor]',
host: {'class': 'dropdown-toggle', 'aria-haspopup': 'true', '[attr.aria-expanded]': 'dropdown.isOpen()'}
})
export class NgbDropdownAnchor {
anchorEl;

constructor(@Inject(forwardRef(() => NgbDropdown)) public dropdown, private _elementRef: ElementRef) {
this.anchorEl = _elementRef.nativeElement;
}

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

/**
* Allows the dropdown to be toggled via click. This directive is optional: you can use NgbDropdownAnchor as an
* alternative.
*/
@Directive({
selector: '[ngbDropdownToggle]',
Expand All @@ -59,18 +80,13 @@ export class NgbDropdownMenu {
'aria-haspopup': 'true',
'[attr.aria-expanded]': 'dropdown.isOpen()',
'(click)': 'toggleOpen()'
}
},
providers: [{provide: NgbDropdownAnchor, useExisting: forwardRef(() => NgbDropdownToggle)}]
})
export class NgbDropdownToggle {
anchorEl;

constructor(@Inject(forwardRef(() => NgbDropdown)) public dropdown, private _elementRef: ElementRef) {
this.anchorEl = _elementRef.nativeElement;
}
export class NgbDropdownToggle extends NgbDropdownAnchor {
constructor(@Inject(forwardRef(() => NgbDropdown)) dropdown, elementRef: ElementRef) { super(dropdown, elementRef); }

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

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

/**
Expand All @@ -90,7 +106,7 @@ export class NgbDropdown implements OnInit {

@ContentChild(NgbDropdownMenu) private _menu: NgbDropdownMenu;

@ContentChild(NgbDropdownToggle) private _toggle: NgbDropdownToggle;
@ContentChild(NgbDropdownAnchor) private _anchor: NgbDropdownAnchor;

/**
* Indicates that dropdown should be closed when selecting one of dropdown items (click) or pressing ESC.
Expand Down Expand Up @@ -189,13 +205,13 @@ export class NgbDropdown implements OnInit {

ngOnDestroy() { this._zoneSubscription.unsubscribe(); }

private _isEventFromToggle($event) { return this._toggle ? this._toggle.isEventFrom($event) : false; }
private _isEventFromToggle($event) { return this._anchor.isEventFrom($event); }

private _isEventFromMenu($event) { return this._menu ? this._menu.isEventFrom($event) : false; }

private _positionMenu() {
if (this.isOpen() && this._menu && this._toggle) {
this._menu.position(this._toggle.anchorEl, this.placement);
if (this.isOpen() && this._menu) {
this._menu.position(this._anchor.anchorEl, this.placement);
}
}
}

0 comments on commit 25e0d39

Please sign in to comment.