Skip to content

Commit

Permalink
[Tooltips] Add tooltips on hover (#6756)
Browse files Browse the repository at this point in the history
* Add tooltip api, extend object api to add telemetry composition lookups, and add tooltips to gauges/notebook embeds/plot legends/object frames/object names/time strips/recent objects/search results/object tree

* Add tooltips to telemetry/lad tables

* Styling normalization, sanding and polishing.

* Add tooltips for Conditional widgets and Tab Views

* Add tests

* Switch to using enum-ish consts for tooltip locations

* Trim LAD table row name to account for spacing required by linting rules

---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
  • Loading branch information
khalidadil and charlesh88 committed Jul 14, 2023
1 parent 795d7a7 commit cde8fbb
Show file tree
Hide file tree
Showing 30 changed files with 942 additions and 26 deletions.
9 changes: 1 addition & 8 deletions e2e/appActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,14 +401,7 @@ async function setEndOffset(page, offset) {
async function selectInspectorTab(page, name) {
const inspectorTabs = page.getByRole('tablist');
const inspectorTab = inspectorTabs.getByTitle(name);
const inspectorTabClass = await inspectorTab.getAttribute('class');
const isSelectedInspectorTab = inspectorTabClass.includes('is-current');

// do not click a tab that is already selected or it will timeout your test
// do to a { pointer-events: none; } on selected tabs
if (!isSelectedInspectorTab) {
await inspectorTab.click();
}
await inspectorTab.click();
}

/**
Expand Down
398 changes: 398 additions & 0 deletions e2e/tests/functional/tooltips.e2e.spec.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions openmct.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ if (document.currentScript) {
* @property {import('./src/api/notifications/NotificationAPI').default} notifications
* @property {import('./src/api/Editor').default} editor
* @property {import('./src/api/overlays/OverlayAPI')} overlays
* @property {import('./src/api/tooltips/ToolTipAPI')} tooltips
* @property {import('./src/api/menu/MenuAPI').default} menus
* @property {import('./src/api/actions/ActionsAPI').default} actions
* @property {import('./src/api/status/StatusAPI').default} status
Expand Down
4 changes: 4 additions & 0 deletions src/MCT.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ define([
'EventEmitter',
'./api/api',
'./api/overlays/OverlayAPI',
'./api/tooltips/ToolTipAPI',
'./selection/Selection',
'./plugins/plugins',
'./ui/registries/ViewRegistry',
Expand All @@ -48,6 +49,7 @@ define([
EventEmitter,
api,
OverlayAPI,
ToolTipAPI,
Selection,
plugins,
ViewRegistry,
Expand Down Expand Up @@ -220,6 +222,8 @@ define([

['overlays', () => new OverlayAPI.default()],

['tooltips', () => new ToolTipAPI.default()],

['menus', () => new api.MenuAPI(this)],

['actions', () => new api.ActionsAPI(this)],
Expand Down
34 changes: 34 additions & 0 deletions src/api/objects/ObjectAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,40 @@ export default class ObjectAPI {
.join('/');
}

/**
* Return path of telemetry objects in the object composition
* @param {object} identifier the identifier for the domain object to query for
* @param {object} [telemetryIdentifier] the specific identifier for the telemetry
* to look for in the composition, uses first object in composition otherwise
* @returns {Array} path of telemetry object in object composition
*/
async getTelemetryPath(identifier, telemetryIdentifier) {
const objectDetails = await this.get(identifier);
const telemetryPath = [];
if (objectDetails.composition && !['folder'].includes(objectDetails.type)) {
let sourceTelemetry = objectDetails.composition[0];
if (telemetryIdentifier) {
sourceTelemetry = objectDetails.composition.find(
(telemetrySource) =>
this.makeKeyString(telemetrySource) === this.makeKeyString(telemetryIdentifier)
);
}
const compositionElement = await this.get(sourceTelemetry);
if (!['yamcs.telemetry', 'generator'].includes(compositionElement.type)) {
return telemetryPath;
}
const telemetryKey = compositionElement.identifier.key;
const telemetryPathObjects = await this.getOriginalPath(telemetryKey);
telemetryPathObjects.forEach((pathObject) => {
if (pathObject.type === 'root') {
return;
}
telemetryPath.unshift(pathObject.name);
});
}
return telemetryPath;
}

/**
* Modify a domain object. Internal to ObjectAPI, won't call save after.
* @private
Expand Down
73 changes: 73 additions & 0 deletions src/api/tooltips/ToolTip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is 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.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/

import TooltipComponent from './components/TooltipComponent.vue';
import EventEmitter from 'EventEmitter';
import Vue from 'vue';

class Tooltip extends EventEmitter {
constructor(
{ toolTipText, toolTipLocation, parentElement } = {
tooltipText: '',
toolTipLocation: 'below',
parentElement: null
}
) {
super();

this.container = document.createElement('div');

this.component = new Vue({
components: {
TooltipComponent: TooltipComponent
},
provide: {
toolTipText,
toolTipLocation,
parentElement
},
template: '<tooltip-component toolTipText="toolTipText"></tooltip-component>'
});

this.isActive = null;
}

destroy() {
if (!this.isActive) {
return;
}
document.body.removeChild(this.container);
this.component.$destroy();
this.isActive = false;
}

/**
* @private
**/
show() {
document.body.appendChild(this.container);
this.container.appendChild(this.component.$mount().$el);
this.isActive = true;
}
}

export default Tooltip;
90 changes: 90 additions & 0 deletions src/api/tooltips/ToolTipAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is 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.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/

import Tooltip from './ToolTip';

/**
* @readonly
* @enum {String} TooltipLocation
* @property {String} ABOVE The string for locating tooltips above an element
* @property {String} BELOW The string for locating tooltips below an element
* @property {String} RIGHT The pixel-spatial annotation type
* @property {String} LEFT The temporal annotation type
* @property {String} CENTER The plot-spatial annotation type
*/
const TOOLTIP_LOCATIONS = Object.freeze({
ABOVE: 'above',
BELOW: 'below',
RIGHT: 'right',
LEFT: 'left',
CENTER: 'center'
});

/**
* The TooltipAPI is responsible for adding custom tooltips to
* the desired elements on the screen
*
* @memberof api/tooltips
* @constructor
*/

class TooltipAPI {
constructor() {
this.activeToolTips = [];
this.TOOLTIP_LOCATIONS = TOOLTIP_LOCATIONS;
}

/**
* @private for platform-internal use
*/
showTooltip(tooltip) {
for (let i = this.activeToolTips.length - 1; i > -1; i--) {
this.activeToolTips[i].destroy();
this.activeToolTips.splice(i, 1);
}
this.activeToolTips.push(tooltip);

tooltip.show();
}

/**
* A description of option properties that can be passed into the tooltip
* @typedef {Object} TooltipOptions
* @property {string} tooltipText text to show in the tooltip
* @property {TOOLTIP_LOCATIONS} tooltipLocation location to show the tooltip relative to the parentElement
* @property {HTMLElement} parentElement reference to the DOM node we're adding the tooltip to
*/

/**
* Tooltips take an options object that consists of the string, tooltipLocation, and parentElement
* @param {TooltipOptions} options
*/
tooltip(options) {
let tooltip = new Tooltip(options);

this.showTooltip(tooltip);

return tooltip;
}
}

export default TooltipAPI;
61 changes: 61 additions & 0 deletions src/api/tooltips/components/TooltipComponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<!--
Open MCT, Copyright (c) 2014-2023, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is 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.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div ref="tooltip-wrapper" class="c-menu c-tooltip-wrapper" :style="toolTipLocationStyle">
<div class="c-tooltip">
{{ toolTipText }}
</div>
</div>
</template>

<script>
export default {
inject: ['toolTipText', 'toolTipLocation', 'parentElement'],
computed: {
toolTipCoordinates() {
return this.parentElement.getBoundingClientRect();
},
toolTipLocationStyle() {
const { top, left, height, width } = this.toolTipCoordinates;
let toolTipLocationStyle = {};
if (this.toolTipLocation === 'above') {
toolTipLocationStyle = { top: `${top - 5}px`, left: `${left}px` };
}
if (this.toolTipLocation === 'below') {
toolTipLocationStyle = { top: `${top + height}px`, left: `${left}px` };
}
if (this.toolTipLocation === 'right') {
toolTipLocationStyle = { top: `${top}px`, left: `${left + width}px` };
}
if (this.toolTipLocation === 'left') {
toolTipLocationStyle = { top: `${top}px`, left: `${left - width}px` };
}
if (this.toolTipLocation === 'center') {
toolTipLocationStyle = { top: `${top + height / 2}px`, left: `${left + width / 2}px` };
}
return toolTipLocationStyle;
}
}
};
</script>
8 changes: 8 additions & 0 deletions src/api/tooltips/components/tooltip-component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.c-tooltip-wrapper {
max-width: 200px;
padding: $interiorMargin;
}

.c-tooltip {
font-style: italic;
}
72 changes: 72 additions & 0 deletions src/api/tooltips/tooltipMixins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is 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.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/

const tooltipHelpers = {
methods: {
async getTelemetryPathString(telemetryIdentifier) {
let telemetryPathString = '';
if (!this.domainObject?.identifier) {
return;
}
const telemetryPath = await this.openmct.objects.getTelemetryPath(
this.domainObject.identifier,
telemetryIdentifier
);
if (telemetryPath.length) {
telemetryPathString = telemetryPath.join(' / ');
}
return telemetryPathString;
},
async getObjectPath(objectIdentifier) {
if (!objectIdentifier && !this.domainObject) {
return;
}
const domainObjectIdentifier = objectIdentifier || this.domainObject.identifier;
const objectPathList = await this.openmct.objects.getOriginalPath(domainObjectIdentifier);
objectPathList.pop();
return objectPathList
.map((pathItem) => pathItem.name)
.reverse()
.join(' / ');
},
buildToolTip(tooltipText, tooltipLocation, elementRef) {
if (!tooltipText || tooltipText.length < 1) {
return;
}
let parentElement = this.$refs[elementRef];
if (Array.isArray(parentElement)) {
parentElement = parentElement[0];
}
this.tooltip = this.openmct.tooltips.tooltip({
toolTipText: tooltipText,
toolTipLocation: tooltipLocation,
parentElement: parentElement
});
},
hideToolTip() {
this.tooltip?.destroy();
this.tooltip = null;
}
}
};

export default tooltipHelpers;

0 comments on commit cde8fbb

Please sign in to comment.