diff --git a/src/api/menu/MenuAPI.js b/src/api/menu/MenuAPI.js index 51f176feb3..bf20c8a4fd 100644 --- a/src/api/menu/MenuAPI.js +++ b/src/api/menu/MenuAPI.js @@ -20,7 +20,25 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import Menu from './menu.js'; +import Menu, { MENU_PLACEMENT } from './menu.js'; + +/** + * Popup Menu options + * @typedef {Object} MenuOptions + * @property {String} menuClass Class for popup menu + * @property {MENU_PLACEMENT} placement Placement for menu relative to click + * @property {Function} onDestroy callback function: invoked when menu is destroyed + */ + +/** + * Popup Menu Item/action + * @typedef {Object} Action + * @property {String} cssClass Class for menu item + * @property {Boolean} isDisabled adds disable class if true + * @property {String} name Menu item text + * @property {String} description Menu item description + * @property {Function} callBack callback function: invoked when item is clicked + */ /** * The MenuAPI allows the addition of new context menu actions, and for the context menu to be launched from @@ -33,12 +51,46 @@ class MenuAPI { constructor(openmct) { this.openmct = openmct; + this.menuPlacement = MENU_PLACEMENT; this.showMenu = this.showMenu.bind(this); + this.showSuperMenu = this.showSuperMenu.bind(this); + this._clearMenuComponent = this._clearMenuComponent.bind(this); this._showObjectMenu = this._showObjectMenu.bind(this); } - showMenu(x, y, actions, onDestroy) { + /** + * Show popup menu + * @param {number} x x-coordinates for popup + * @param {number} y x-coordinates for popup + * @param {Array.|Array.>} actions collection of actions{@link Action} or collection of groups of actions {@link Action} + * @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu + */ + showMenu(x, y, actions, menuOptions) { + this._createMenuComponent(x, y, actions, menuOptions); + + this.menuComponent.showMenu(); + } + + /** + * Show popup menu with description of item on hover + * @param {number} x x-coordinates for popup + * @param {number} y x-coordinates for popup + * @param {Array.|Array.>} actions collection of actions {@link Action} or collection of groups of actions {@link Action} + * @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu + */ + showSuperMenu(x, y, actions, menuOptions) { + this._createMenuComponent(x, y, actions, menuOptions); + + this.menuComponent.showSuperMenu(); + } + + _clearMenuComponent() { + this.menuComponent = undefined; + delete this.menuComponent; + } + + _createMenuComponent(x, y, actions, menuOptions = {}) { if (this.menuComponent) { this.menuComponent.dismiss(); } @@ -47,18 +99,13 @@ class MenuAPI { x, y, actions, - onDestroy + ...menuOptions }; this.menuComponent = new Menu(options); this.menuComponent.once('destroy', this._clearMenuComponent); } - _clearMenuComponent() { - this.menuComponent = undefined; - delete this.menuComponent; - } - _showObjectMenu(objectPath, x, y, actionsToBeIncluded) { let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(objectPath, actionsToBeIncluded); diff --git a/src/api/menu/MenuAPISpec.js b/src/api/menu/MenuAPISpec.js index 6c8c6db61d..b5c21ed809 100644 --- a/src/api/menu/MenuAPISpec.js +++ b/src/api/menu/MenuAPISpec.js @@ -22,10 +22,11 @@ import MenuAPI from './MenuAPI'; import Menu from './menu'; -import { createOpenMct, resetApplicationState } from '../../utils/testing'; +import { createOpenMct, createMouseEvent, resetApplicationState } from '../../utils/testing'; describe ('The Menu API', () => { let openmct; + let element; let menuAPI; let actionsArray; let x; @@ -33,21 +34,37 @@ describe ('The Menu API', () => { let result; let onDestroy; - beforeEach(() => { + beforeEach((done) => { + const appHolder = document.createElement('div'); + appHolder.style.display = 'block'; + appHolder.style.width = '1920px'; + appHolder.style.height = '1080px'; + openmct = createOpenMct(); + + element = document.createElement('div'); + element.style.display = 'block'; + element.style.width = '1920px'; + element.style.height = '1080px'; + + openmct.on('start', done); + openmct.startHeadless(appHolder); + menuAPI = new MenuAPI(openmct); actionsArray = [ { + key: 'test-css-class-1', name: 'Test Action 1', - cssClass: 'test-css-class-1', + cssClass: 'icon-clock', description: 'This is a test action', callBack: () => { result = 'Test Action 1 Invoked'; } }, { + key: 'test-css-class-2', name: 'Test Action 2', - cssClass: 'test-css-class-2', + cssClass: 'icon-clock', description: 'This is a test action', callBack: () => { result = 'Test Action 2 Invoked'; @@ -76,7 +93,11 @@ describe ('The Menu API', () => { beforeEach(() => { onDestroy = jasmine.createSpy('onDestroy'); - menuAPI.showMenu(x, y, actionsArray, onDestroy); + const menuOptions = { + onDestroy + }; + + menuAPI.showMenu(x, y, actionsArray, menuOptions); vueComponent = menuAPI.menuComponent.component; menuComponent = document.querySelector(".c-menu"); @@ -131,4 +152,62 @@ describe ('The Menu API', () => { }); }); }); + + describe("superMenu method", () => { + it("creates a superMenu", () => { + menuAPI.showSuperMenu(x, y, actionsArray); + + const superMenu = document.querySelector('.c-super-menu__menu'); + + expect(superMenu).not.toBeNull(); + }); + + it("Mouse over a superMenu shows correct description", (done) => { + menuAPI.showSuperMenu(x, y, actionsArray); + + const superMenu = document.querySelector('.c-super-menu__menu'); + const superMenuItem = superMenu.querySelector('li'); + const mouseOverEvent = createMouseEvent('mouseover'); + + superMenuItem.dispatchEvent(mouseOverEvent); + const itemDescription = document.querySelector('.l-item-description__description'); + + setTimeout(() => { + expect(itemDescription.innerText).toEqual(actionsArray[0].description); + expect(superMenu).not.toBeNull(); + done(); + }, 300); + }); + }); + + describe("Menu Placements", () => { + it("default menu position BOTTOM_RIGHT", () => { + menuAPI.showMenu(x, y, actionsArray); + + const menu = document.querySelector('.c-menu'); + + const boundingClientRect = menu.getBoundingClientRect(); + const left = boundingClientRect.left; + const top = boundingClientRect.top; + + expect(left).toEqual(x); + expect(top).toEqual(y); + }); + + it("menu position BOTTOM_RIGHT", () => { + const menuOptions = { + placement: openmct.menus.menuPlacement.BOTTOM_RIGHT + }; + + menuAPI.showMenu(x, y, actionsArray, menuOptions); + + const menu = document.querySelector('.c-menu'); + const boundingClientRect = menu.getBoundingClientRect(); + const left = boundingClientRect.left; + const top = boundingClientRect.top; + + expect(left).toEqual(x); + expect(top).toEqual(y); + }); + }); }); diff --git a/src/api/menu/components/Menu.vue b/src/api/menu/components/Menu.vue index 7d687cbbe0..a9d22cff0a 100644 --- a/src/api/menu/components/Menu.vue +++ b/src/api/menu/components/Menu.vue @@ -1,8 +1,10 @@