Skip to content

Commit

Permalink
feat(context-menu): add nbContextMenuTrigger parameter (#1139)
Browse files Browse the repository at this point in the history
Closes #1112
  • Loading branch information
nnixaa authored Jan 9, 2019
1 parent 113d3b0 commit 27b291e
Show file tree
Hide file tree
Showing 10 changed files with 405 additions and 14 deletions.
12 changes: 12 additions & 0 deletions src/app/playground-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,18 @@ export const PLAYGROUND_COMPONENTS: ComponentLink[] = [
component: 'ContextMenuTestComponent',
name: 'Context Menu Test',
},
{
path: 'context-menu-modes.component',
link: '/context-menu/context-menu-modes.component',
component: 'ContextMenuModesComponent',
name: 'Context Menu Modes',
},
{
path: 'context-menu-noop.component',
link: '/context-menu/context-menu-noop.component',
component: 'ContextMenuNoopComponent',
name: 'Context Menu Noop',
},
],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,23 @@ import { NB_DOCUMENT } from '../../theme.options';
* ```ts
* items = [{ title: 'Profile' }, { title: 'Log out' }];
* ```
* Context menu has a number of triggers which provides an ability to show and hide the component in different ways:
*
* - Click mode shows the component when a user clicks on the host element and hides when the user clicks
* somewhere on the document outside the component.
* - Hint provides capability to show the component when the user hovers over the host element
* and hide when the user hovers out of the host.
* - Hover works like hint mode with one exception - when the user moves mouse from host element to
* the container element the component remains open, so that it is possible to interact with it content.
* - Focus mode is applied when user focuses the element.
* - Noop mode - the component won't react to the user interaction.
*
* @stacked-example(Available Triggers, context-menu/context-menu-modes.component.html)
*
* Noop mode is especially useful when you need to control Popover programmatically, for example show/hide
* as a result of some third-party action, like HTTP request or validation check:
*
* @stacked-example(Manual Control, context-menu/context-menu-noop.component)
* */
@Directive({ selector: '[nbContextMenu]' })
export class NbContextMenuDirective implements AfterViewInit, OnDestroy {
Expand Down Expand Up @@ -122,6 +139,13 @@ export class NbContextMenuDirective implements AfterViewInit, OnDestroy {
this.items = items;
};

/**
* Describes when the container will be shown.
* Available options: `click`, `hover`, `hint`, `focus` and `noop`
* */
@Input('nbContextMenuTrigger')
trigger: NbTrigger = NbTrigger.CLICK;

protected ref: NbOverlayRef;
protected container: ComponentRef<any>;
protected positionStrategy: NbAdjustableConnectedPositionStrategy;
Expand All @@ -140,11 +164,15 @@ export class NbContextMenuDirective implements AfterViewInit, OnDestroy {
ngAfterViewInit() {
this.subscribeOnTriggers();
this.subscribeOnItemClick();
this.subscribeOnPositionChange();
}

ngOnDestroy() {
this.alive = false;
this.hide();
if (this.ref) {
this.ref.dispose();
}
}

show() {
Expand Down Expand Up @@ -172,12 +200,10 @@ export class NbContextMenuDirective implements AfterViewInit, OnDestroy {
}

protected createOverlay() {
this.positionStrategy = this.createPositionStrategy();
this.ref = this.overlay.create({
positionStrategy: this.positionStrategy,
scrollStrategy: this.overlay.scrollStrategies.reposition(),
});
this.subscribeOnPositionChange();
}

protected openContextMenu() {
Expand All @@ -197,13 +223,14 @@ export class NbContextMenuDirective implements AfterViewInit, OnDestroy {

protected createTriggerStrategy(): NbTriggerStrategy {
return this.triggerStrategyBuilder
.trigger(NbTrigger.CLICK)
.trigger(this.trigger)
.host(this.hostRef.nativeElement)
.container(() => this.container)
.build();
}

protected subscribeOnPositionChange() {
this.positionStrategy = this.createPositionStrategy();
this.positionStrategy.positionChange
.pipe(takeWhile(() => this.alive))
.subscribe((position: NbPosition) => patch(this.container, { position }));
Expand Down
234 changes: 234 additions & 0 deletions src/framework/theme/components/context-menu/context-menu.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { Component, ComponentRef, ElementRef, Inject, Injectable, Input, NgModule, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

import { of as observableOf, Subject } from 'rxjs';

import { NbThemeModule } from '../../theme.module';
import { NbLayoutModule } from '../layout/layout.module';
import {
NbAdjustment,
NbPosition,
NbPositionBuilderService,
NbTrigger,
NbTriggerStrategy,
NbTriggerStrategyBuilderService,
} from '../cdk';
import { NB_DOCUMENT } from '../../theme.options';
import { NbContextMenuDirective } from './context-menu.directive';
import { NbMenuModule } from '../menu/menu.module';
import { NbContextMenuModule } from './context-menu.module';


@Component({
selector: 'nb-context-menu-test',
template: `
<nb-layout>
<nb-layout-column>
<button #button [nbContextMenu]="items" [nbContextMenuTrigger]="trigger">
</button>
</nb-layout-column>
</nb-layout>
`,
})
export class NbContextMenuTestComponent {
@Input() trigger: NbTrigger = NbTrigger.CLICK;
@ViewChild('button') button: ElementRef;
@ViewChild(NbContextMenuDirective) contextMenu: NbContextMenuDirective;

items = [{ title: 'User' }, { title: 'Log Out' }];
}

@NgModule({
imports: [
RouterTestingModule.withRoutes([]),
NoopAnimationsModule,
NbThemeModule.forRoot(),
NbLayoutModule,
NbMenuModule.forRoot(),
NbContextMenuModule,
],
declarations: [
NbContextMenuTestComponent,
],
})
export class ContextMenuTestModule {
}

export class MockPositionBuilder {
positionChange = new Subject();
_connectedTo: ElementRef<any>;
_position: NbPosition;
_adjustment: NbAdjustment;

connectedTo(connectedTo: ElementRef<any>) {
this._connectedTo = connectedTo;
return this;
}

position(position: NbPosition) {
this._position = position;
return this;
}

adjustment(adjustment: NbAdjustment) {
this._adjustment = adjustment;
return this;
}

offset() {
return this;
};

attach() {
};

apply() {
};

detach() {
};

dispose() {
};
}

@Injectable()
export class MockTriggerStrategyBuilder {

_host: HTMLElement;
_container: () => ComponentRef<any>;
_trigger: NbTrigger;

constructor(@Inject(NB_DOCUMENT) public _document: Document) {
}

trigger(trigger: NbTrigger): this {
this._trigger = trigger;
return this;
}

host(host: HTMLElement): this {
this._host = host;
return this;
}

container(container: () => ComponentRef<any>): this {
this._container = container;
return this;
}

build(): NbTriggerStrategy {
return {
show$: observableOf(null),
hide$: observableOf(null),
} as NbTriggerStrategy;
}
}

describe('Directive: NbContextMenuDirective', () => {
beforeEach(() => {
TestBed.configureTestingModule({ imports: [ContextMenuTestModule] });
});

let fixture: ComponentFixture<NbContextMenuTestComponent>;

beforeEach(() => {
fixture = TestBed.createComponent(NbContextMenuTestComponent);
fixture.detectChanges();
});

afterEach(() => {
fixture.destroy();
});

it('should render context menu', () => {
fixture.componentInstance.contextMenu.show();
fixture.detectChanges();

const menu = fixture.nativeElement.querySelector('nb-context-menu nb-menu');
expect(menu).toBeTruthy();
});

it('should hide', fakeAsync(() => {
let menu;
fixture.componentInstance.contextMenu.show();
fixture.detectChanges();


menu = fixture.nativeElement.querySelector('nb-context-menu nb-menu');
expect(menu).toBeTruthy();
fixture.componentInstance.contextMenu.hide();
fixture.detectChanges();

tick(); // we need this tick for animations
menu = fixture.nativeElement.querySelector('nb-context-menu nb-menu');
expect(menu).toBeFalsy();
}));

it('should toogle', fakeAsync(() => {
let menu;

fixture.componentInstance.contextMenu.show();
fixture.detectChanges();

menu = fixture.nativeElement.querySelector('nb-context-menu nb-menu');
expect(menu).toBeTruthy();
fixture.componentInstance.contextMenu.toggle();
fixture.detectChanges();

tick(); // we need this tick for animations
const tooltip = fixture.nativeElement.querySelector('nb-context-menu');
expect(tooltip).toBeNull();

fixture.componentInstance.contextMenu.toggle();
fixture.detectChanges();
tick();

menu = fixture.nativeElement.querySelector('nb-context-menu nb-menu');
expect(menu).toBeTruthy();
}));

it('should build position strategy', () => {
const mockPositionBuilder = new MockPositionBuilder();
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [ContextMenuTestModule],
providers: [{ provide: NbPositionBuilderService, useValue: mockPositionBuilder }],
});
fixture = TestBed.createComponent(NbContextMenuTestComponent);
fixture.detectChanges();

expect(mockPositionBuilder._connectedTo.nativeElement).toBe(fixture.componentInstance.button.nativeElement);
expect(mockPositionBuilder._position).toBe(NbPosition.BOTTOM);
expect(mockPositionBuilder._adjustment).toBe(NbAdjustment.CLOCKWISE);
});

it('should build with default trigger strategy', () => {
TestBed.resetTestingModule();
const bed = TestBed.configureTestingModule({
imports: [ContextMenuTestModule],
providers: [{ provide: NbTriggerStrategyBuilderService, useClass: MockTriggerStrategyBuilder }],
});
const mockTriggerStrategy = bed.get(NbTriggerStrategyBuilderService);
fixture = TestBed.createComponent(NbContextMenuTestComponent);
fixture.detectChanges();

expect(mockTriggerStrategy._trigger).toBe(NbTrigger.CLICK);
});

it('should build with custom trigger strategy', () => {
TestBed.resetTestingModule();
const bed = TestBed.configureTestingModule({
imports: [ContextMenuTestModule],
providers: [{ provide: NbTriggerStrategyBuilderService, useClass: MockTriggerStrategyBuilder }],
});
const mockTriggerStrategy = bed.get(NbTriggerStrategyBuilderService);
fixture = TestBed.createComponent(NbContextMenuTestComponent);
fixture.componentInstance.trigger = NbTrigger.HOVER;
fixture.detectChanges();

expect(mockTriggerStrategy._trigger).toBe(NbTrigger.HOVER);
});
});
20 changes: 10 additions & 10 deletions src/framework/theme/components/popover/popover.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,23 @@ import { NbPopoverComponent } from './popover.component';
* <button nbPopover="Hello, Popover!" [nbPopoverAdjust]="false"></button>
* ```
*
* Also popover has some different modes which provides capability show$ and hide$ popover in different ways:
* Popover has a number of triggers which provides an ability to show and hide the component in different ways:
*
* - Click popover mode shows when a user clicking on the host element and hides when the user clicks
* somewhere on the document except popover.
* - Hint mode provides capability show$ popover when the user hovers on the host element
* and hide$ popover when user hovers out of the host.
* - Hover mode works like hint mode with one exception - when the user moves mouse from host element to
* the container element popover will not be hidden.
* - Click mode shows the component when a user clicks on the host element and hides when the user clicks
* somewhere on the document outside the component.
* - Hint provides capability to show the component when the user hovers over the host element
* and hide when the user hovers out of the host.
* - Hover works like hint mode with one exception - when the user moves mouse from host element to
* the container element the component remains open, so that it is possible to interact with it content.
* - Focus mode is applied when user focuses the element.
* - Noop mode - the popover won't react based on user interaction.
* - Noop mode - the component won't react to the user interaction.
*
* @stacked-example(Available Modes, popover/popover-modes.component.html)
* @stacked-example(Available Triggers, popover/popover-modes.component.html)
*
* Noop mode is especially useful when you need to control Popover programmatically, for example show/hide
* as a result of some third-party action, like HTTP request or validation check:
*
* @stacked-example(Manual control, popover/popover-noop.component)
* @stacked-example(Manual Control, popover/popover-noop.component)
*
* @additional-example(Template Ref, popover/popover-template-ref.component)
* @additional-example(Custom Component, popover/popover-custom-component.component)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<nb-layout>
<nb-layout-column>
<button nbButton outline [nbContextMenu]="items" nbContextMenuTrigger="click">
Click Mode
</button>

<button nbButton outline [nbContextMenu]="items" nbContextMenuTrigger="focus">
Focus (Tab) Mode
</button>

<button nbButton outline [nbContextMenu]="items" nbContextMenuTrigger="hover">
Hover Mode
</button>
</nb-layout-column>
</nb-layout>
Loading

0 comments on commit 27b291e

Please sign in to comment.