diff --git a/docs/context-menu.directive.md b/docs/context-menu.directive.md index 03d789de17..3f0dd41fe0 100644 --- a/docs/context-menu.directive.md +++ b/docs/context-menu.directive.md @@ -45,6 +45,12 @@ export class MyComponent implements OnInit { } ``` +### Properties + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| showIcons | boolean | false | Render defined icons | + ## Details See **Demo Shell** or **DocumentList** implementation for more details and use cases. diff --git a/ng2-components/ng2-alfresco-core/src/components/context-menu/context-menu-holder.component.spec.ts b/ng2-components/ng2-alfresco-core/src/components/context-menu/context-menu-holder.component.spec.ts index 401bd526c0..3264847114 100644 --- a/ng2-components/ng2-alfresco-core/src/components/context-menu/context-menu-holder.component.spec.ts +++ b/ng2-components/ng2-alfresco-core/src/components/context-menu/context-menu-holder.component.spec.ts @@ -15,99 +15,248 @@ * limitations under the License. */ +import { OverlayContainer } from '@angular/cdk/overlay'; +import { ViewportRuler } from '@angular/cdk/scrolling'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ContextMenuHolderComponent } from './context-menu-holder.component'; +import { ContextMenuModule } from './context-menu.module'; import { ContextMenuService } from './context-menu.service'; describe('ContextMenuHolderComponent', () => { + let fixture: ComponentFixture; + let component: ContextMenuHolderComponent; + let contextMenuService: ContextMenuService; + let overlayContainer = { + getContainerElement: () => ({ + querySelector: (val) => ({ + name: val, + clientWidth: 0, + clientHeight: 0, + parentElement: { + style: { + left: 0, + top: 0 + } + } + }) - let contextMenuService; - let menuHolder; + }) + }; + + let getViewportRect = { + getViewportRect: () => ({ + left: 0, top: 0, width: 1014, height: 686, bottom: 0, right: 0 + }) + }; beforeEach(() => { - contextMenuService = new ContextMenuService(); - menuHolder = new ContextMenuHolderComponent(contextMenuService); - }); - - it('should show menu on service event', () => { - spyOn(menuHolder, 'showMenu').and.callThrough(); - contextMenuService.show.next({}); - - expect(menuHolder.showMenu).toHaveBeenCalled(); - }); - - it('should have fixed position', () => { - expect(menuHolder.locationCss).toEqual( - jasmine.objectContaining({ - position: 'fixed' - }) - ); - }); - - it('should setup empty location by default', () => { - expect(menuHolder.locationCss).toEqual( - jasmine.objectContaining({ - left: '0px', - top: '0px' - }) - ); - }); - - it('should be hidden by default', () => { - expect(menuHolder.isShown).toBeFalsy(); - expect(menuHolder.locationCss).toEqual( - jasmine.objectContaining({ - display: 'none' - }) - ); - }); - - it('should show on service event', () => { - expect(menuHolder.isShown).toBeFalsy(); - contextMenuService.show.next({}); - expect(menuHolder.isShown).toBeTruthy(); - }); - - it('should update position from service event', () => { - - expect(menuHolder.locationCss).toEqual( - jasmine.objectContaining({ - left: '0px', - top: '0px' - }) - ); - - let event = new MouseEvent('click', { - clientX: 10, - clientY: 20 + TestBed.configureTestingModule({ + imports: [ + ContextMenuModule + ], + providers: [ + { + provide: OverlayContainer, + useValue: overlayContainer + }, + { + provide: ViewportRuler, + useValue: getViewportRect + } + ] }); - contextMenuService.show.next({ event: event }); + fixture = TestBed.createComponent(ContextMenuHolderComponent); + component = fixture.componentInstance; + contextMenuService = TestBed.get(ContextMenuService); - expect(menuHolder.locationCss).toEqual( - jasmine.objectContaining({ - left: '10px', - top: '20px' - }) - ); + fixture.detectChanges(); }); - it('should take links from service event', () => { - let links = [{}, {}]; - contextMenuService.show.next({ obj: links }); - expect(menuHolder.links).toBe(links); + beforeEach(() => { + spyOn(component.menuTrigger, 'openMenu'); }); - it('should hide on outside click', () => { - contextMenuService.show.next({}); - expect(menuHolder.isShown).toBeTruthy(); + describe('Events', () => { + it('should show menu on service event', () => { + spyOn(component, 'showMenu'); - menuHolder.clickedOutside(); - expect(menuHolder.isShown).toBeFalsy(); - expect(menuHolder.locationCss).toEqual( - jasmine.objectContaining({ - display: 'none' - }) - ); + contextMenuService.show.next( {}); + + expect(component.showMenu).toHaveBeenCalled(); + }); + + it('should set DOM element reference on menu open event', () => { + component.menuTrigger.onMenuOpen.next(); + + expect(component.mdMenuElement.name).toBe('.context-menu'); + }); + + it('should reset DOM element reference on menu close event', () => { + component.menuTrigger.onMenuClose.next(); + + expect(component.mdMenuElement).toBe(null); + }); }); + describe('onMenuItemClick()', () => { + const menuItem = { + model: { + disabled: false + }, + subject: { + next: (val) => val + } + }; + + const event = { + preventDefault: () => null, + stopImmediatePropagation: () => null + }; + + beforeEach(() => { + spyOn(menuItem.subject, 'next'); + }); + + it('should emit when link is not disabled', () => { + component.onMenuItemClick( event, menuItem); + + expect(menuItem.subject.next).toHaveBeenCalledWith(menuItem); + }); + + it('should not emit when link is disabled', () => { + menuItem.model.disabled = true; + component.onMenuItemClick( event, menuItem); + + expect(menuItem.subject.next).not.toHaveBeenCalled(); + }); + }); + + describe('showMenu()', () => { + it('should open menu panel', () => { + component.showMenu( {}, [{}]); + + expect(component.menuTrigger.openMenu).toHaveBeenCalled(); + }); + }); + + describe('Menu position', () => { + beforeEach(() => { + component.menuTrigger.onMenuOpen.next(); + component.mdMenuElement.clientHeight = 160; + component.mdMenuElement.clientWidth = 200; + }); + + it('should set position to mouse position', () => { + const contextMenuEvent = { + clientX: 100, + clientY: 210 + }; + + component.showMenu( contextMenuEvent, [{}]); + + expect(component.mdMenuElement.parentElement.style).toEqual({ + left: '100px', + top: '210px' + }); + }); + + it('should ajust position relative to right margin of the screen', () => { + const contextMenuEvent = { + clientX: 1000, + clientY: 210 + }; + + component.showMenu( contextMenuEvent, [{}]); + + expect(component.mdMenuElement.parentElement.style).toEqual({ + left: '800px', + top: '210px' + }); + }); + + it('should ajust position relative to bottom margin of the screen', () => { + const contextMenuEvent = { + clientX: 100, + clientY: 600 + }; + + component.showMenu( contextMenuEvent, [{}]); + + expect(component.mdMenuElement.parentElement.style).toEqual({ + left: '100px', + top: '440px' + }); + }); + + it('should ajust position relative to bottom - right margin of the screen', () => { + const contextMenuEvent = { + clientX: 900, + clientY: 610 + }; + + component.showMenu( contextMenuEvent, [{}]); + + expect(component.mdMenuElement.parentElement.style).toEqual({ + left: '700px', + top: '450px' + }); + }); + }); + + describe('Menu direction', () => { + beforeEach(() => { + component.menuTrigger.onMenuOpen.next(); + component.mdMenuElement.clientHeight = 160; + component.mdMenuElement.clientWidth = 200; + }); + + it('should set default menu direction', () => { + const contextMenuEvent = { + clientX: 100, + clientY: 210 + }; + + component.showMenu( contextMenuEvent, [{}]); + + expect(component.menuTrigger.menu.xPosition).toBe('after'); + expect(component.menuTrigger.menu.yPosition).toBe('below'); + }); + + it('should ajust direction relative to right margin of the screen', () => { + const contextMenuEvent = { + clientX: 1000, + clientY: 210 + }; + + component.showMenu( contextMenuEvent, [{}]); + + expect(component.menuTrigger.menu.xPosition).toBe('before'); + expect(component.menuTrigger.menu.yPosition).toBe('below'); + }); + + it('should ajust direction relative to bottom margin of the screen', () => { + const contextMenuEvent = { + clientX: 100, + clientY: 600 + }; + + component.showMenu( contextMenuEvent, [{}]); + + expect(component.menuTrigger.menu.xPosition).toBe('after'); + expect(component.menuTrigger.menu.yPosition).toBe('above'); + }); + + it('should ajust position relative to bottom - right margin of the screen', () => { + const contextMenuEvent = { + clientX: 900, + clientY: 610 + }; + + component.showMenu( contextMenuEvent, [{}]); + + expect(component.menuTrigger.menu.xPosition).toBe('before'); + expect(component.menuTrigger.menu.yPosition).toBe('above'); + }); + }); }); diff --git a/ng2-components/ng2-alfresco-core/src/components/context-menu/context-menu-holder.component.ts b/ng2-components/ng2-alfresco-core/src/components/context-menu/context-menu-holder.component.ts index dab6292023..fc687ed48e 100644 --- a/ng2-components/ng2-alfresco-core/src/components/context-menu/context-menu-holder.component.ts +++ b/ng2-components/ng2-alfresco-core/src/components/context-menu/context-menu-holder.component.ts @@ -15,74 +15,73 @@ * limitations under the License. */ -import { Component, HostListener } from '@angular/core'; +import { OverlayContainer } from '@angular/cdk/overlay'; +import { ViewportRuler } from '@angular/cdk/scrolling'; +import { Component, HostListener, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { MdMenuTrigger } from '@angular/material'; +import { Subscription } from 'rxjs/Rx'; import { ContextMenuService } from './context-menu.service'; @Component({ selector: 'adf-context-menu-holder, context-menu-holder', - styles: [ - ` - .menu-container { - background: #fff; - display: block; - margin: 0; - padding: 0; - border: none; - overflow: visible; - z-index: 9999; - } - - .context-menu { - list-style-type: none; - position: static; - height: auto; - width: auto; - min-width: 124px; - padding: 8px 0; - margin: 0; - box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); - border-radius: 2px; - } - - .context-menu .link { - opacity: 1; - } - ` - ], template: ` - + + ` }) -export class ContextMenuHolderComponent { +export class ContextMenuHolderComponent implements OnInit, OnDestroy { links = []; - isShown = false; + private mouseLocation: { left: number, top: number } = {left: 0, top: 0}; + private menuElement = null; + private openSubscription: Subscription; + private closeSubscription: Subscription; + private contextSubscription: Subscription; - constructor(contextMenuService: ContextMenuService) { - contextMenuService.show.subscribe(e => this.showMenu(e.event, e.obj)); + @Input() showIcons: boolean = false; + @ViewChild(MdMenuTrigger) menuTrigger: MdMenuTrigger; + + @HostListener('contextmenu', ['$event']) + onShowContextMenu(event?: MouseEvent) { + if (event) { + event.preventDefault(); + } } - get locationCss() { - return { - position: 'fixed', - display: this.isShown ? 'block' : 'none', - left: this.mouseLocation.left + 'px', - top: this.mouseLocation.top + 'px' - }; + @HostListener('window:resize', ['$event']) + onResize(event) { + if (this.mdMenuElement) { + this.setPosition(); + } } - @HostListener('document:click') - clickedOutside() { - this.isShown = false; + constructor( + private viewport: ViewportRuler, + private overlayContainer: OverlayContainer, + private contextMenuService: ContextMenuService + ) {} + + ngOnInit() { + this.contextSubscription = this.contextMenuService.show.subscribe(e => this.showMenu(e.event, e.obj)); + this.openSubscription = this.menuTrigger.onMenuOpen.subscribe(() => this.menuElement = this.getContextMenuElement()); + this.closeSubscription = this.menuTrigger.onMenuClose.subscribe(() => this.menuElement = null); + } + + ngOnDestroy() { + this.contextSubscription.unsubscribe(); + this.openSubscription.unsubscribe(); + this.closeSubscription.unsubscribe(); } onMenuItemClick(event: Event, menuItem: any): void { @@ -95,7 +94,6 @@ export class ContextMenuHolderComponent { } showMenu(e, links) { - this.isShown = true; this.links = links; if (e) { @@ -104,12 +102,44 @@ export class ContextMenuHolderComponent { top: e.clientY }; } - } - @HostListener('contextmenu', ['$event']) - onShowContextMenu(event?: MouseEvent) { - if (event) { - event.preventDefault(); + this.menuTrigger.openMenu(); + + if (this.mdMenuElement) { + this.setPosition(); } } + + get mdMenuElement() { + return this.menuElement; + } + + private locationCss() { + return { + left: this.mouseLocation.left + 'px', + top: this.mouseLocation.top + 'px' + }; + } + + private setPosition() { + if (this.mdMenuElement.clientWidth + this.mouseLocation.left > this.viewport.getViewportRect().width) { + this.menuTrigger.menu.xPosition = 'before'; + this.mdMenuElement.parentElement.style.left = this.mouseLocation.left - this.mdMenuElement.clientWidth + 'px'; + } else { + this.menuTrigger.menu.xPosition = 'after'; + this.mdMenuElement.parentElement.style.left = this.locationCss().left; + } + + if (this.mdMenuElement.clientHeight + this.mouseLocation.top > this.viewport.getViewportRect().height) { + this.menuTrigger.menu.yPosition = 'above'; + this.mdMenuElement.parentElement.style.top = this.mouseLocation.top - this.mdMenuElement.clientHeight + 'px'; + } else { + this.menuTrigger.menu.yPosition = 'below'; + this.mdMenuElement.parentElement.style.top = this.locationCss().top; + } + } + + private getContextMenuElement() { + return this.overlayContainer.getContainerElement().querySelector('.context-menu'); + } } diff --git a/ng2-components/ng2-alfresco-core/src/components/context-menu/context-menu.module.ts b/ng2-components/ng2-alfresco-core/src/components/context-menu/context-menu.module.ts index 73aafbc21c..f26ac53473 100644 --- a/ng2-components/ng2-alfresco-core/src/components/context-menu/context-menu.module.ts +++ b/ng2-components/ng2-alfresco-core/src/components/context-menu/context-menu.module.ts @@ -17,6 +17,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MdButtonModule, MdIconModule, MdMenuModule } from '@angular/material'; import { ContextMenuHolderComponent } from './context-menu-holder.component'; import { ContextMenuDirective } from './context-menu.directive'; @@ -24,7 +25,10 @@ import { ContextMenuService } from './context-menu.service'; @NgModule({ imports: [ - CommonModule + CommonModule, + MdButtonModule, + MdIconModule, + MdMenuModule ], declarations: [ ContextMenuHolderComponent,