Skip to content
This repository has been archived by the owner on Mar 27, 2023. It is now read-only.

Commit

Permalink
[NG] Datagrid: Fix performance issue for action overflow
Browse files Browse the repository at this point in the history
We now properly add the click listener on the whole document only
when the action overflow menu is open.

Fixes #1111.

Signed-off-by: Eudes Petonnet-Vincent <[email protected]>
  • Loading branch information
youdz committed Jun 28, 2017
1 parent 8b2672b commit c4424bf
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 29 deletions.
2 changes: 1 addition & 1 deletion src/app/datagrid/selection/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,4 @@ export class DatagridSelectionDemo {
this.cleanUp();
this.toAdd = this.selected.slice();
}
}
}
48 changes: 21 additions & 27 deletions src/clarity-angular/data/datagrid/datagrid-action-overflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
* The full license information can be found in LICENSE in the root directory of this project.
*/
import {
Component, EventEmitter, HostListener, Input, Output, ElementRef
Component, EventEmitter, Input, Output, ElementRef
} from "@angular/core";
import {Point} from "../../popover/common/popover";

@Component({
selector: "clr-dg-action-overflow",
template: `
<clr-icon #anchor shape="ellipsis-vertical" class="datagrid-action-toggle" (click)="toggle()"></clr-icon>
<clr-icon #anchor shape="ellipsis-vertical" class="datagrid-action-toggle" (click)="toggle($event)"></clr-icon>
<ng-template [(clrPopover)]="open" [clrPopoverAnchor]="anchor" [clrPopoverAnchorPoint]="anchorPoint"
[clrPopoverPopoverPoint]="popoverPoint">
<div #menu class="datagrid-action-overflow">
<div #menu class="datagrid-action-overflow" (clrOutsideClick)="close($event)" [clrStrict]="true">
<ng-content></ng-content>
</div>
</ng-template>
Expand Down Expand Up @@ -48,36 +48,30 @@ export class DatagridActionOverflow {

@Output("clrDgActionOverflowOpenChange") public openChanged = new EventEmitter<boolean>(false);

/*
* We need to remember the click that opens the menu, to make sure it doesn't close the menu instantly
* when the event bubbles up the DOM all the way to the document, which we also listen to.
*/
private openingEvent: any;

/**
* Shows/hides the action overflow menu
*/
public toggle() {
public toggle(event: any) {
this.openingEvent = event;
this.open = !this.open;
}

//called on mouse clicks anywhere in the DOM.
//Checks to see if the mouseclick happened on the host or outside
@HostListener("document:click", [ "$event.target" ])
onMouseClick(target: any): void {
if (this._open) {
let current: any = target; //Get the element in the DOM on which the mouse was clicked
let actionMenuHost: any = this.elementRef.nativeElement; //Get the current actionMenu native HTML element

if (target.className === "datagrid-action-overflow") {
return; // if clicking on the action overflow container but not the content, return without closing
}

//Start checking if current and actionMenuHost are equal. If not traverse to the parentNode and check again.
while (current) {
if (current.className === "datagrid-action-overflow") {
break; // if user clicked on the overflow menu, hide it
}
if (current === actionMenuHost) {
return;
}
current = current.parentNode;
}
this._open = false; // Hide the overflow menu
public close(event: MouseEvent) {
/*
* Because this listener is added synchonously, before the event finishes bubbling up the DOM,
* we end up firing on the very click that just opened the menu, p
* otentially closing it immediately every time. So we just ignore it.
*/
if (event === this.openingEvent) {
delete this.openingEvent;
return;
}
this.open = false;
}
}
4 changes: 3 additions & 1 deletion src/clarity-angular/data/datagrid/datagrid.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { FormsModule } from "@angular/forms";
import { ClrCommonPopoverModule } from "../../popover/common/popover.module";
import { ClrLoadingModule } from "../../utils/loading/loading.module";
import { DATAGRID_DIRECTIVES } from "./index";
import { ClrOutsideClickModule } from "../../utils/outside-click/outside-click.module";

@NgModule({
imports: [
Expand All @@ -20,7 +21,8 @@ import { DATAGRID_DIRECTIVES } from "./index";
ClrFormsModule,
FormsModule,
ClrCommonPopoverModule,
ClrLoadingModule
ClrLoadingModule,
ClrOutsideClickModule
],
declarations: [
DATAGRID_DIRECTIVES,
Expand Down
13 changes: 13 additions & 0 deletions src/clarity-angular/utils/outside-click/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2016 VMware, Inc. All Rights Reserved.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
import { Type } from "@angular/core";
import {OutsideClick} from "./outside-click";

export * from "./outside-click";

export const OUSTIDE_CLICK_DIRECTIVES: Type<any>[] = [
OutsideClick
];
15 changes: 15 additions & 0 deletions src/clarity-angular/utils/outside-click/outside-click.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2016-2017 VMware, Inc. All Rights Reserved.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import {OUSTIDE_CLICK_DIRECTIVES} from "./index";

@NgModule({
imports: [CommonModule],
declarations: [OUSTIDE_CLICK_DIRECTIVES],
exports: [OUSTIDE_CLICK_DIRECTIVES]
})
export class ClrOutsideClickModule {}
72 changes: 72 additions & 0 deletions src/clarity-angular/utils/outside-click/outside-click.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2016 VMware, Inc. All Rights Reserved.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
import {Component} from "@angular/core";
import {TestBed} from "@angular/core/testing";
import {By} from "@angular/platform-browser";
import {OutsideClick} from "./outside-click";

describe("Loading directive", function() {
beforeEach(function () {
TestBed.configureTestingModule({
declarations: [OutsideClick, FullTest]
});
this.fixture = TestBed.createComponent(FullTest);
this.fixture.detectChanges();
this.testComponent = this.fixture.componentInstance;
this.host = this.fixture.debugElement.query(By.css(".host")).nativeElement;
this.button = this.fixture.debugElement.query(By.css("button")).nativeElement;
this.outside = this.fixture.debugElement.query(By.css(".outside")).nativeElement;
});

afterEach(function() {
this.fixture.destroy();
});

it("emits clicks outside of the host", function () {
expect(this.testComponent.nbClicks).toBe(0);
this.outside.click();
expect(this.testComponent.nbClicks).toBe(1);
this.outside.click();
expect(this.testComponent.nbClicks).toBe(2);
});

it("ignores clicks inside of the host", function () {
expect(this.testComponent.nbClicks).toBe(0);
this.host.click();
expect(this.testComponent.nbClicks).toBe(0);
this.button.click();
expect(this.testComponent.nbClicks).toBe(0);
});

it("offers a strict input to only ignore clicks that happen exactly on the host", function () {
this.testComponent.strict = true;
this.fixture.detectChanges();
expect(this.testComponent.nbClicks).toBe(0);
this.host.click();
expect(this.testComponent.nbClicks).toBe(0);
this.button.click();
expect(this.testComponent.nbClicks).toBe(1);
});
});


@Component({
template: `
<p class="outside">Hello World</p>
<p class="host" (clrOutsideClick)="inc()" [clrStrict]="strict">
<button>Button</button>
</p>
`
})
class FullTest {
public strict = false;
public nbClicks = 0;

inc() {
this.nbClicks++;
}
}
26 changes: 26 additions & 0 deletions src/clarity-angular/utils/outside-click/outside-click.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {Directive, Output, EventEmitter, HostListener, ElementRef, Input} from "@angular/core";

@Directive({
selector: "[clrOutsideClick]"
})
export class OutsideClick {
constructor(private el: ElementRef) {}

@Input("clrStrict") strict = false;

@Output("clrOutsideClick") outsideClick = new EventEmitter<any>(false);

@HostListener("document:click", ["$event"])
documentClick(event: MouseEvent) {
let target = event.target; //Get the element in the DOM on which the mouse was clicked
let host = this.el.nativeElement; //Get the current actionMenu native HTML element

if (target === host) {
return;
}
if (!this.strict && host.contains(target)) {
return;
}
this.outsideClick.emit(event);
}
}

0 comments on commit c4424bf

Please sign in to comment.