diff --git a/e2e/actions/users.actions.ts b/e2e/actions/users.actions.ts index ee7cae38e5..744e510e50 100644 --- a/e2e/actions/users.actions.ts +++ b/e2e/actions/users.actions.ts @@ -21,7 +21,7 @@ import TestConfig = require('../test.config'); import path = require('path'); import fs = require('fs'); import remote = require('selenium-webdriver/remote'); -import { browser } from "protractor"; +import { browser } from 'protractor'; export class UsersActions { diff --git a/e2e/pages/adf/dialog/contentList.js b/e2e/pages/adf/dialog/contentList.js index 0393252a73..e22dcc0b8f 100644 --- a/e2e/pages/adf/dialog/contentList.js +++ b/e2e/pages/adf/dialog/contentList.js @@ -319,11 +319,11 @@ var ContentList = function () { this.rightClickOnRowNamed = function(rowName) { let row = this.getRowByRowName(rowName); browser.actions().click(row, protractor.Button.RIGHT).perform(); - Util.waitUntilElementIsVisible(element(by.css('div.context-menu'))); + Util.waitUntilElementIsVisible(element(by.id('adf-context-menu-content'))); } this.checkContextActionIsVisible = function(actionName) { - let actionButton = element(by.css(`div.context-menu button[data-automation-id="context-${actionName}"`)); + let actionButton = element(by.css(`button[data-automation-id="context-${actionName}"`)); Util.waitUntilElementIsVisible(actionButton); Util.waitUntilElementIsClickable(actionButton); return actionButton; diff --git a/e2e/standalone_task.e2e.ts b/e2e/process-services/standalone_task.e2e.ts similarity index 90% rename from e2e/standalone_task.e2e.ts rename to e2e/process-services/standalone_task.e2e.ts index b45bf24425..3f84097d09 100644 --- a/e2e/standalone_task.e2e.ts +++ b/e2e/process-services/standalone_task.e2e.ts @@ -17,23 +17,23 @@ import { browser } from 'protractor'; -import LoginPage = require('./pages/adf/loginPage'); -import ProcessServicesPage = require('./pages/adf/process_services/processServicesPage'); -import TasksPage = require('./pages/adf/process_services/tasksPage'); +import LoginPage = require('../pages/adf/loginPage'); +import ProcessServicesPage = require('../pages/adf/process_services/processServicesPage'); +import TasksPage = require('../pages/adf/process_services/tasksPage'); -import CONSTANTS = require('./util/constants'); +import CONSTANTS = require('../util/constants'); -import Tenant = require('./models/APS/Tenant'); -import Task = require('./models/APS/Task'); +import Tenant = require('../models/APS/Tenant'); +import Task = require('../models/APS/Task'); -import TestConfig = require('./test.config'); -import resources = require('./util/resources'); +import TestConfig = require('../test.config'); +import resources = require('../util/resources'); -import AlfrescoApi = require('alfresco-js-api-node'); -import { UsersActions } from './actions/users.actions'; +import AlfrescoApi = require('.alfresco-js-api-node'); +import { UsersActions } from '../actions/users.actions'; import fs = require('fs'); import path = require('path'); -import Util = require('./util/util'); +import Util = require('..ro/util/util'); describe('Start Task - Task App', () => { diff --git a/e2e/task-audit.e2e.ts b/e2e/process-services/task-audit.e2e.ts similarity index 90% rename from e2e/task-audit.e2e.ts rename to e2e/process-services/task-audit.e2e.ts index 50866bd470..3bd9d5933f 100644 --- a/e2e/task-audit.e2e.ts +++ b/e2e/process-services/task-audit.e2e.ts @@ -15,20 +15,20 @@ * limitations under the License. */ -import LoginPage = require('./pages/adf/loginPage'); -import ProcessServicesPage = require('./pages/adf/process_services/processServicesPage'); -import TasksPage = require('./pages/adf/process_services/tasksPage'); +import LoginPage = require('../pages/adf/loginPage'); +import ProcessServicesPage = require('../pages/adf/process_services/processServicesPage'); +import TasksPage = require('../pages/adf/process_services/tasksPage'); -import CONSTANTS = require('./util/constants'); +import CONSTANTS = require('../util/constants'); -import Tenant = require('./models/APS/Tenant'); +import Tenant = require('../models/APS/Tenant'); -import TestConfig = require('./test.config'); -import resources = require('./util/resources'); +import TestConfig = require('../test.config'); +import resources = require('../util/resources'); import AlfrescoApi = require('alfresco-js-api-node'); -import { UsersActions } from './actions/users.actions'; -import { AppsActions } from './actions/APS/apps.actions'; +import { UsersActions } from '../actions/users.actions'; +import { AppsActions } from '../actions/APS/apps.actions'; import path = require('path'); import Util = require('./util/util'); diff --git a/e2e/task_filters_component.e2e.ts b/e2e/process-services/task_filters_component.e2e.ts similarity index 90% rename from e2e/task_filters_component.e2e.ts rename to e2e/process-services/task_filters_component.e2e.ts index d4eaae38ab..e0b342bf52 100644 --- a/e2e/task_filters_component.e2e.ts +++ b/e2e/process-services/task_filters_component.e2e.ts @@ -15,19 +15,19 @@ * limitations under the License. */ -import TestConfig = require('./test.config'); -import resources = require('./util/resources'); -import LoginPage = require('./pages/adf/loginPage'); -import NavigationBarPage = require('./pages/adf/navigationBarPage'); -import ProcessServicesPage = require('./pages/adf/process_services/processServicesPage'); -import TasksPage = require('./pages/adf/process_services/tasksPage'); -import TasksListPage = require('./pages/adf/process_services/tasksListPage'); -import TaskFiltersPage = require('./pages/adf/process_services/taskFiltersPage'); -import TaskDetailsPage = require('./pages/adf/process_services/taskDetailsPage'); +import TestConfig = require('../test.config'); +import resources = require('../util/resources'); +import LoginPage = require('../pages/adf/loginPage'); +import NavigationBarPage = require('../pages/adf/navigationBarPage'); +import ProcessServicesPage = require('../pages/adf/process_services/processServicesPage'); +import TasksPage = require('../pages/adf/process_services/tasksPage'); +import TasksListPage = require('../pages/adf/process_services/tasksListPage'); +import TaskFiltersPage = require('../pages/adf/process_services/taskFiltersPage'); +import TaskDetailsPage = require('../pages/adf/process_services/taskDetailsPage'); import AlfrescoApi = require('alfresco-js-api-node'); -import { AppsActions } from './actions/APS/apps.actions'; -import { UsersActions } from './actions/users.actions'; +import { AppsActions } from '../actions/APS/apps.actions'; +import { UsersActions } from '../actions/users.actions'; describe('Task Filters Test', () => { diff --git a/lib/core/context-menu/animations.ts b/lib/core/context-menu/animations.ts new file mode 100644 index 0000000000..33bb01173b --- /dev/null +++ b/lib/core/context-menu/animations.ts @@ -0,0 +1,46 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + state, + style, + animate, + transition, + query, + group, + sequence, + AnimationStateMetadata, + AnimationTransitionMetadata +} from '@angular/animations'; + +export const contextMenuAnimation: ( AnimationStateMetadata | AnimationTransitionMetadata)[] = [ + state('void', style({ + opacity: 0, + transform: 'scale(0.01, 0.01)' + })), + transition('void => *', sequence([ + query('.mat-menu-content', style({ opacity: 0 })), + animate('100ms linear', style({ opacity: 1, transform: 'scale(1, 0.5)' })), + group([ + query('.mat-menu-content', animate('400ms cubic-bezier(0.55, 0, 0.55, 0.2)', + style({ opacity: 1 }) + )), + animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({ transform: 'scale(1, 1)' })) + ]) + ])), + transition('* => void', animate('150ms 50ms linear', style({ opacity: 0 }))) +]; diff --git a/lib/core/context-menu/context-menu-list.component.ts b/lib/core/context-menu/context-menu-list.component.ts new file mode 100644 index 0000000000..24acc6cef3 --- /dev/null +++ b/lib/core/context-menu/context-menu-list.component.ts @@ -0,0 +1,101 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Component, ViewEncapsulation, HostListener, AfterViewInit, + Optional, Inject, QueryList, ViewChildren +} from '@angular/core'; +import { trigger } from '@angular/animations'; +import { DOWN_ARROW, UP_ARROW } from '@angular/cdk/keycodes'; +import { FocusKeyManager } from '@angular/cdk/a11y'; +import { MatMenuItem } from '@angular/material'; +import { ContextMenuOverlayRef } from './context-menu-overlay'; +import { contextMenuAnimation } from './animations'; +import { CONTEXT_MENU_DATA } from './context-menu.tokens'; + +@Component({ + selector: 'adf-context-menu', + template: ` +
+
+ + + +
+
+ `, + host: { + role: 'menu', + class: 'adf-context-menu' + }, + encapsulation: ViewEncapsulation.None, + animations: [ + trigger('panelAnimation', contextMenuAnimation) + ] +}) +export class ContextMenuListComponent implements AfterViewInit { + private keyManager: FocusKeyManager; + @ViewChildren(MatMenuItem) items: QueryList; + links: any[]; + + @HostListener('document:keydown.Escape', ['$event']) + handleKeydownEscape(event: KeyboardEvent) { + if (event) { + this.contextMenuOverlayRef.close(); + } + } + + @HostListener('document:keydown', ['$event']) + handleKeydownEvent(event: KeyboardEvent) { + if (event) { + const keyCode = event.keyCode; + if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) { + this.keyManager.onKeydown(event); + } + } + } + + constructor( + @Inject(ContextMenuOverlayRef) private contextMenuOverlayRef: ContextMenuOverlayRef, + @Optional() @Inject(CONTEXT_MENU_DATA) private data: any + ) { + this.links = this.data; + } + + onMenuItemClick(event: Event, menuItem: any) { + if (menuItem && menuItem.model && menuItem.model.disabled) { + event.preventDefault(); + event.stopImmediatePropagation(); + return; + } + + menuItem.subject.next(menuItem); + this.contextMenuOverlayRef.close(); + } + + ngAfterViewInit() { + this.keyManager = new FocusKeyManager(this.items); + this.keyManager.setFirstItemActive(); + } +} diff --git a/lib/core/context-menu/context-menu-overlay.service.ts b/lib/core/context-menu/context-menu-overlay.service.ts new file mode 100644 index 0000000000..fb1641eca6 --- /dev/null +++ b/lib/core/context-menu/context-menu-overlay.service.ts @@ -0,0 +1,129 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable, Injector, ElementRef, ComponentRef } from '@angular/core'; +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { PortalInjector, ComponentPortal } from '@angular/cdk/portal'; +import { ContextMenuOverlayRef } from './context-menu-overlay'; +import { ContextMenuOverlayConfig } from './interfaces'; +import { CONTEXT_MENU_DATA } from './context-menu.tokens'; +import { ContextMenuListComponent } from './context-menu-list.component'; + +const DEFAULT_CONFIG: ContextMenuOverlayConfig = { + panelClass: 'cdk-overlay-pane', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true +}; + +@Injectable() +export class ContextMenuOverlayService { + + constructor( private injector: Injector, private overlay: Overlay) { } + + open(config: ContextMenuOverlayConfig): ContextMenuOverlayRef { + const overlayConfig = { ...DEFAULT_CONFIG, ...config }; + + const overlay = this.createOverlay(overlayConfig); + + const overlayRef = new ContextMenuOverlayRef(overlay); + + this.attachDialogContainer(overlay, config, overlayRef); + + overlay.backdropClick().subscribe(() => overlayRef.close()); + + // prevent native contextmenu on overlay element if config.hasBackdrop is true + if (overlayConfig.hasBackdrop) { + ( overlay)._backdropElement + .addEventListener('contextmenu', (event) => { + event.preventDefault(); + ( overlay)._backdropClick.next(null); + }, true); + } + + return overlayRef; + } + + private createOverlay(config: ContextMenuOverlayConfig): OverlayRef { + const overlayConfig = this.getOverlayConfig(config); + return this.overlay.create(overlayConfig); + } + + private attachDialogContainer(overlay: OverlayRef, config: ContextMenuOverlayConfig, contextMenuOverlayRef: ContextMenuOverlayRef) { + const injector = this.createInjector(config, contextMenuOverlayRef); + + const containerPortal = new ComponentPortal(ContextMenuListComponent, null, injector); + const containerRef: ComponentRef = overlay.attach(containerPortal); + + return containerRef.instance; + } + + private createInjector(config: ContextMenuOverlayConfig, contextMenuOverlayRef: ContextMenuOverlayRef): PortalInjector { + const injectionTokens = new WeakMap(); + + injectionTokens.set(ContextMenuOverlayRef, contextMenuOverlayRef); + injectionTokens.set(CONTEXT_MENU_DATA, config.data); + + return new PortalInjector(this.injector, injectionTokens); + } + + private getOverlayConfig(config: ContextMenuOverlayConfig): OverlayConfig { + const { clientY, clientX } = config.source; + + const fakeElement: any = { + getBoundingClientRect: (): ClientRect => ({ + bottom: clientY, + height: 0, + left: clientX, + right: clientX, + top: clientY, + width: 0 + }) + }; + + const positionStrategy = this.overlay.position() + .connectedTo( + new ElementRef(fakeElement), + { originX: 'start', originY: 'bottom' }, + { overlayX: 'start', overlayY: 'top' }) + .withFallbackPosition( + { originX: 'start', originY: 'top' }, + { overlayX: 'start', overlayY: 'bottom' }) + .withFallbackPosition( + { originX: 'end', originY: 'top' }, + { overlayX: 'start', overlayY: 'top' }) + .withFallbackPosition( + { originX: 'start', originY: 'top' }, + { overlayX: 'end', overlayY: 'top' }) + .withFallbackPosition( + { originX: 'end', originY: 'center' }, + { overlayX: 'start', overlayY: 'center' }) + .withFallbackPosition( + { originX: 'start', originY: 'center' }, + { overlayX: 'end', overlayY: 'center' } + ); + + const overlayConfig = new OverlayConfig({ + hasBackdrop: config.hasBackdrop, + backdropClass: config.backdropClass, + panelClass: config.panelClass, + scrollStrategy: this.overlay.scrollStrategies.close(), + positionStrategy + }); + + return overlayConfig; + } +} diff --git a/lib/core/context-menu/context-menu-overlay.ts b/lib/core/context-menu/context-menu-overlay.ts new file mode 100644 index 0000000000..fa5bdba518 --- /dev/null +++ b/lib/core/context-menu/context-menu-overlay.ts @@ -0,0 +1,27 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { OverlayRef } from '@angular/cdk/overlay'; + +export class ContextMenuOverlayRef { + + constructor(private overlayRef: OverlayRef) { } + + close(): void { + this.overlayRef.dispose(); + } +} diff --git a/lib/core/context-menu/context-menu.directive.spec.ts b/lib/core/context-menu/context-menu.directive.spec.ts deleted file mode 100644 index 608d622a27..0000000000 --- a/lib/core/context-menu/context-menu.directive.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*! - * @license - * Copyright 2016 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ContextMenuDirective } from './context-menu.directive'; -import { ContextMenuService } from './context-menu.service'; - -describe('ContextMenuDirective', () => { - - let contextMenuService; - let directive; - - beforeEach(() => { - contextMenuService = new ContextMenuService(); - directive = new ContextMenuDirective(contextMenuService); - directive.enabled = true; - }); - - it('should show menu via service', (done) => { - contextMenuService.show.subscribe(() => { - done(); - }); - - directive.links = [{}]; - directive.onShowContextMenu(null); - }); - - it('should prevent default behavior', () => { - let event = new MouseEvent('click'); - spyOn(event, 'preventDefault').and.callThrough(); - - directive.onShowContextMenu(event); - expect(event.preventDefault).toHaveBeenCalled(); - }); - - it('should forward event to service', () => { - let event = new MouseEvent('click'); - - contextMenuService.show.subscribe(e => { - expect(e.event).toBeDefined(); - expect(e.event).toBe(event); - }); - - directive.onShowContextMenu(event); - }); - - it('should forward menu items to service', () => { - let links = [{}, {}]; - directive.links = links; - - contextMenuService.show.subscribe(e => { - expect(e.obj).toBeDefined(); - expect(e.obj).toBe(links); - }); - - directive.onShowContextMenu(null); - }); - -}); diff --git a/lib/core/context-menu/context-menu.directive.ts b/lib/core/context-menu/context-menu.directive.ts index 4e8dfe77e1..864aa565cb 100644 --- a/lib/core/context-menu/context-menu.directive.ts +++ b/lib/core/context-menu/context-menu.directive.ts @@ -18,7 +18,7 @@ /* tslint:disable:no-input-rename */ import { Directive, HostListener, Input } from '@angular/core'; -import { ContextMenuService } from './context-menu.service'; +import { ContextMenuOverlayService } from './context-menu-overlay.service'; // @deprecated 2.3.0 context-menu tag removed @Directive({ @@ -33,8 +33,7 @@ export class ContextMenuDirective { @Input('context-menu-enabled') enabled: boolean = false; - constructor(private _contextMenuService: ContextMenuService) { - } + constructor(private contextMenuService: ContextMenuOverlayService) {} @HostListener('contextmenu', ['$event']) onShowContextMenu(event?: MouseEvent) { @@ -44,9 +43,10 @@ export class ContextMenuDirective { } if (this.links && this.links.length > 0) { - if (this._contextMenuService) { - this._contextMenuService.show.next({event: event, obj: this.links}); - } + this.contextMenuService.open({ + source: event, + data: this.links + }); } } } diff --git a/lib/core/context-menu/context-menu.module.ts b/lib/core/context-menu/context-menu.module.ts index 6636fb0185..d253b62502 100644 --- a/lib/core/context-menu/context-menu.module.ts +++ b/lib/core/context-menu/context-menu.module.ts @@ -22,6 +22,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { ContextMenuHolderComponent } from './context-menu-holder.component'; import { ContextMenuDirective } from './context-menu.directive'; +import { ContextMenuListComponent } from './context-menu-list.component'; @NgModule({ imports: [ @@ -31,11 +32,15 @@ import { ContextMenuDirective } from './context-menu.directive'; ], declarations: [ ContextMenuHolderComponent, - ContextMenuDirective + ContextMenuDirective, + ContextMenuListComponent ], exports: [ ContextMenuHolderComponent, ContextMenuDirective + ], + entryComponents: [ + ContextMenuListComponent ] }) export class ContextMenuModule {} diff --git a/lib/core/context-menu/context-menu.spec.ts b/lib/core/context-menu/context-menu.spec.ts new file mode 100644 index 0000000000..bd45c075b3 --- /dev/null +++ b/lib/core/context-menu/context-menu.spec.ts @@ -0,0 +1,174 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ContextMenuModule } from './context-menu.module'; +import { CoreModule } from '../core.module'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +@Component({ + selector: 'adf-test-component', + template: ` +
+ ` +}) +class TestComponent { + actions; +} + +describe('ContextMenuDirective', () => { + let fixture: ComponentFixture; + const actions = [ + { + model: { + visible: false, + title: 'Action 1' + }, + subject: { + next: jasmine.createSpy('next') + } + }, + { + model: { + visible: true, + disabled: true, + title: 'Action 2', + icon: null + }, + subject: { + next: jasmine.createSpy('next') + } + }, + { + model: { + visible: true, + disabled: false, + title: 'Action 3', + icon: 'action-icon-3' + }, + subject: { + next: jasmine.createSpy('next') + } + }, + { + model: { + visible: true, + disabled: false, + title: 'Action 4', + icon: 'action-icon-4' + }, + subject: { + next: jasmine.createSpy('next') + } + } + ]; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + CoreModule.forRoot(), + ContextMenuModule, + NoopAnimationsModule + ], + declarations: [ + TestComponent + ] + }); + + fixture = TestBed.createComponent(TestComponent); + fixture.componentInstance.actions = actions; + fixture.detectChanges(); + }); + + it('should not render contextmenu when action was not performed', () => { + const containerElement = fixture.debugElement.nativeElement.parentElement; + expect(containerElement.querySelector('.adf-context-menu button')).toBe(null); + }); + + describe('Events', () => { + let targetElement: HTMLElement; + let contextMenu: HTMLElement; + + beforeEach(() => { + targetElement = fixture.debugElement.nativeElement.querySelector('#target'); + targetElement.dispatchEvent(new CustomEvent('contextmenu')); + fixture.detectChanges(); + contextMenu = document.querySelector('.adf-context-menu'); + }); + + it('should show menu on mouse contextmenu event', () => { + expect(contextMenu).not.toBe(null); + }); + + it('should set DOM element reference on menu open event', () => { + expect(contextMenu.className).toContain('adf-context-menu'); + }); + + it('should reset DOM element reference on Escape event', () => { + const event = new KeyboardEvent('keydown', { + bubbles : true, cancelable : true, key : 'Escape' + }); + + document.querySelector('.cdk-overlay-backdrop').dispatchEvent(event); + fixture.detectChanges(); + expect(document.querySelector('.adf-context-menu')).toBe(null); + }); + }); + + describe('Contextmenu list', () => { + let targetElement: HTMLElement; + let contextMenu: HTMLElement; + + beforeEach(() => { + targetElement = fixture.debugElement.nativeElement.querySelector('#target'); + targetElement.dispatchEvent(new CustomEvent('contextmenu')); + fixture.detectChanges(); + contextMenu = document.querySelector('.adf-context-menu'); + }); + + it('should not render item with visibility property set to false', () => { + expect(contextMenu.querySelectorAll('button').length).toBe(3); + }); + + it('should render item as disabled when `disabled` property is set to true', () => { + expect(contextMenu.querySelectorAll('button')[0].disabled).toBe(true); + }); + + it('should set first not disabled item as active', () => { + expect(document.activeElement.querySelector('mat-icon').innerHTML).toContain('action-icon-3'); + }); + + it('should not allow action event when item is disabled', () => { + contextMenu.querySelectorAll('button')[0].click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.actions[1].subject.next).not.toHaveBeenCalled(); + }); + + it('should perform action when item is not disabled', () => { + contextMenu.querySelectorAll('button')[1].click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.actions[2].subject.next).toHaveBeenCalled(); + }); + + it('should not render item icon if not set', () => { + expect(contextMenu.querySelectorAll('button')[0].querySelector('mat-icon')).toBe(null); + }); + }); +}); diff --git a/lib/core/context-menu/context-menu.tokens.ts b/lib/core/context-menu/context-menu.tokens.ts new file mode 100644 index 0000000000..aaaab85592 --- /dev/null +++ b/lib/core/context-menu/context-menu.tokens.ts @@ -0,0 +1,20 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InjectionToken } from '@angular/core'; + +export const CONTEXT_MENU_DATA = new InjectionToken('CONTEXT_MENU_DATA'); diff --git a/lib/core/context-menu/interfaces.ts b/lib/core/context-menu/interfaces.ts new file mode 100644 index 0000000000..623f01da23 --- /dev/null +++ b/lib/core/context-menu/interfaces.ts @@ -0,0 +1,24 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface ContextMenuOverlayConfig { + panelClass?: string; + hasBackdrop?: boolean; + backdropClass?: string; + source?: MouseEvent; + data?: any; +} diff --git a/lib/core/core.module.ts b/lib/core/core.module.ts index cc70998f28..5a2531c365 100644 --- a/lib/core/core.module.ts +++ b/lib/core/core.module.ts @@ -85,6 +85,7 @@ import { startupServiceFactory } from './services/startup-service-factory'; import { SortingPickerModule } from './sorting-picker/sorting-picker.module'; import { AppConfigService } from './app-config/app-config.service'; import { ContextMenuService } from './context-menu/context-menu.service'; +import { ContextMenuOverlayService } from './context-menu/context-menu-overlay.service'; import { ActivitiContentService } from './form/services/activiti-alfresco.service'; import { EcmModelService } from './form/services/ecm-model.service'; import { FormRenderingService } from './form/services/form-rendering.service'; @@ -138,6 +139,7 @@ export function providers() { DatePipe, AppConfigService, ContextMenuService, + ContextMenuOverlayService, ActivitiContentService, EcmModelService, FormRenderingService,