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,