Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Menu API] All our drop down menus should use the new menu api #3607 #3620

Merged
merged 11 commits into from
Mar 29, 2021
63 changes: 55 additions & 8 deletions src/api/menu/MenuAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.<Action>|Array.<Array.<Action>>} 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.<Action>|Array.<Array.<Action>>} 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();
}
Expand All @@ -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);

Expand Down
89 changes: 84 additions & 5 deletions src/api/menu/MenuAPISpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,49 @@

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;
let y;
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';
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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);
});
});
});
16 changes: 9 additions & 7 deletions src/api/menu/components/Menu.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<template>
<div class="c-menu">
<ul v-if="actions.length && actions[0].length">
<div class="c-menu"
:class="options.menuClass"
>
<ul v-if="options.actions.length && options.actions[0].length">
<template
v-for="(actionGroups, index) in actions"
v-for="(actionGroups, index) in options.actions"
>
<li
v-for="action in actionGroups"
Expand All @@ -14,7 +16,7 @@
{{ action.name }}
</li>
<div
v-if="index !== actions.length - 1"
v-if="index !== options.actions.length - 1"
:key="index"
class="c-menu__section-separator"
>
Expand All @@ -30,15 +32,15 @@

<ul v-else>
<li
v-for="action in actions"
v-for="action in options.actions"
:key="action.name"
:class="action.cssClass"
:title="action.description"
@click="action.callBack"
>
{{ action.name }}
</li>
<li v-if="actions.length === 0">
<li v-if="options.actions.length === 0">
No actions defined.
</li>
</ul>
Expand All @@ -47,6 +49,6 @@

<script>
export default {
inject: ['actions']
inject: ['options']
};
</script>
88 changes: 88 additions & 0 deletions src/api/menu/components/SuperMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<template>
<div class="c-menu"
:class="[options.menuClass, 'c-super-menu']"
>
<ul v-if="options.actions.length && options.actions[0].length"
class="c-super-menu__menu"
>
<template
v-for="(actionGroups, index) in options.actions"
>
<li
v-for="action in actionGroups"
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
@click="action.callBack"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</template>
</ul>

<ul v-else
class="c-super-menu__menu"
>
<li
v-for="action in options.actions"
:key="action.name"
:class="action.cssClass"
:title="action.description"
@click="action.callBack"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
{{ action.name }}
</li>
<li v-if="options.actions.length === 0">
No actions defined.
</li>
</ul>

<div class="c-super-menu__item-description">
<div :class="['l-item-description__icon', 'bg-' + hoveredItem.cssClass]"></div>
<div class="l-item-description__name">
{{ hoveredItem.name }}
</div>
<div class="l-item-description__description">
{{ hoveredItem.description }}
</div>
</div>
</div>
</template>

<script>
export default {
inject: ['options'],
data: function () {
return {
hoveredItem: {}
};
},
methods: {
toggleItemDescription(action = {}) {
const hoveredItem = {
name: action.name,
description: action.description,
cssClass: action.cssClass
};

this.hoveredItem = Object.assign({}, this.hoveredItem, hoveredItem);
}
}
};
</script>