fix(#7524): Open in New Tab action from a sub-object in a layout (#7542)

* refactor: url tools use named exports

* fix: refactor method and remove customUrlParams

* test(e2e): verify bounds are preserved in data pivoting

* test: remove test as feature is no longer needed

- dataVisualization logic has moved from MMGIS plugin to the open source. As such, we can just use the time conductor bounds

* refactor: autoformat keeps changing this so i'mma just commit it

* refactor: remove unnecessary code

* refactor: simplify, add docs

* Revert "refactor: remove unnecessary code"

This reverts commit 87aef35c510230835fb682b80e89a6006ef2d923.

* a11y: improve aria labels for ITC

* fix: simplify url method

* fix: update ITC app actions

* test: add test to generate test data for display layout w/ overlay plot + ITC enabled

* test(e2e): add suite + test for open in new tab from subobject

- needs cleanup

* a11y: various a11y improvement drivebys

* a11y: clock indicator needs to be quiet

* a11y: add `aria-live` to SuperMenu details

* a11y: greatly improve a11y of Menus and SuperMenus

* test(e2e): clean up test

* fix: improve a11y for context menus, fix test

* chore: remove nop-longer-recommended extension

* feat: provide one more bound option for example data viz

* fix: no need for `mount`, use dynamic rendering instead

* Revert "fix: simplify url method"

This reverts commit b24c7dabc783a9a1c3f2460eada99f452259f566.

* fix: correct time conductor bounds when opening in a new tab from a plot in the inspector

* test: fix e2e tests

* Revert "test: remove test as feature is no longer needed"

This reverts commit 759ebd4667bffb1979d5f62af6b47f349dcd9f77.

* test: move 2p annotation to test

* test: fix e2e

* fix: no words for the word god today

* test: fix e2e

* fix: e2e test

* test: fix test

* driveby: fix perf test

* fix: revert required prop change

---------

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
This commit is contained in:
Jesse Mazzella 2024-03-11 16:39:38 -07:00 committed by GitHub
parent 0eadc7a4ae
commit 8c2558bfe0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 465 additions and 165 deletions

View File

@ -5,7 +5,6 @@
// List of extensions which should be recommended for users of this workspace. // List of extensions which should be recommended for users of this workspace.
"recommendations": [ "recommendations": [
"Vue.volar", "Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"rvest.vs-code-prettier-eslint" "rvest.vs-code-prettier-eslint"
], ],

View File

@ -505,15 +505,14 @@ async function setTimeConductorBounds(page, startDate, endDate) {
* @param {string} startDate * @param {string} startDate
* @param {string} endDate * @param {string} endDate
*/ */
async function setIndependentTimeConductorBounds(page, startDate, endDate) { async function setIndependentTimeConductorBounds(page, { start, end }) {
// Activate Independent Time Conductor in Fixed Time Mode // Activate Independent Time Conductor
await page.getByRole('switch').click(); await page.getByLabel('Enable Independent Time Conductor').click();
// Bring up the time conductor popup // Bring up the time conductor popup
await page.click('.c-conductor-holder--compact .c-compact-tc'); await page.getByLabel('Independent Time Conductor Settings').click();
await expect(page.locator('.itc-popout')).toBeInViewport(); await expect(page.locator('.itc-popout')).toBeInViewport();
await setTimeBounds(page, start, end);
await setTimeBounds(page, startDate, endDate);
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
} }

File diff suppressed because one or more lines are too long

View File

@ -174,6 +174,6 @@ test.describe('AppActions', () => {
type: 'Folder' type: 'Folder'
}); });
await openObjectTreeContextMenu(page, folder.url); await openObjectTreeContextMenu(page, folder.url);
await expect(page.getByLabel('Menu')).toBeVisible(); await expect(page.getByLabel(`${folder.name} Context Menu`)).toBeVisible();
}); });
}); });

View File

@ -33,7 +33,12 @@
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { createDomainObjectWithDefaults, createExampleTelemetryObject } from '../../appActions.js'; import {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
setIndependentTimeConductorBounds,
setTimeConductorBounds
} from '../../appActions.js';
import { MISSION_TIME } from '../../constants.js'; import { MISSION_TIME } from '../../constants.js';
import { expect, test } from '../../pluginFixtures.js'; import { expect, test } from '../../pluginFixtures.js';
@ -89,6 +94,53 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
}); });
}); });
test('Generate display layout with 1 child overlay plot', async ({ page, context }) => {
const parent = await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Parent Display Layout'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Child Overlay Plot 1',
parent: parent.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Child SWG 1',
parent: overlayPlot.uuid
});
await page.goto(parent.url, { waitUntil: 'domcontentloaded' });
await setIndependentTimeConductorBounds(page, {
start: '2024-11-12 19:11:11.000Z',
end: '2024-11-12 20:11:11.000Z'
});
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
await setTimeConductorBounds(page, NEW_GLOBAL_START_BOUNDS, NEW_GLOBAL_END_BOUNDS);
// Verify that the global time conductor bounds have been updated
expect(
await page.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
).toEqual(NEW_GLOBAL_START_BOUNDS);
expect(
await page.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
).toEqual(NEW_GLOBAL_END_BOUNDS);
//Save localStorage for future test execution
await context.storageState({
path: fileURLToPath(
new URL(
'../../../e2e/test-data/display_layout_with_child_overlay_plot.json',
import.meta.url
)
)
});
});
test('Generate flexible layout with 2 child display layouts', async ({ page, context }) => { test('Generate flexible layout with 2 child display layouts', async ({ page, context }) => {
// Create Display Layout // Create Display Layout
const parent = await createDomainObjectWithDefaults(page, { const parent = await createDomainObjectWithDefaults(page, {

View File

@ -131,7 +131,10 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' '); const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' '); const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString); await setIndependentTimeConductorBounds(page, {
start: startBoundString,
end: endBoundString
});
expect(await activityBounds.count()).toEqual(1); expect(await activityBounds.count()).toEqual(1);
}); });
@ -160,7 +163,10 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' '); const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' '); const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString); await setIndependentTimeConductorBounds(page, {
start: startBoundString,
end: endBoundString
});
// Verify that two events are displayed // Verify that two events are displayed
expect(await activityBounds.count()).toEqual(2); expect(await activityBounds.count()).toEqual(2);

View File

@ -286,7 +286,7 @@ test.describe('Basic Condition Set Use', () => {
await page.locator('button[title="Save"]').click(); await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.click('button[title="Change the current view"]'); await page.getByLabel('Open the View Switcher Menu').click();
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden(); await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible(); await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();

View File

@ -23,6 +23,7 @@ import { fileURLToPath } from 'url';
import { import {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
navigateToObjectWithFixedTimeBounds,
setFixedTimeMode, setFixedTimeMode,
setIndependentTimeConductorBounds, setIndependentTimeConductorBounds,
setRealTimeMode, setRealTimeMode,
@ -30,12 +31,120 @@ import {
} from '../../../../appActions.js'; } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
const LOCALSTORAGE_PATH = fileURLToPath( const CHILD_LAYOUT_STORAGE_STATE_PATH = fileURLToPath(
new URL('../../../../test-data/display_layout_with_child_layouts.json', import.meta.url) new URL('../../../../test-data/display_layout_with_child_layouts.json', import.meta.url)
); );
const CHILD_PLOT_STORAGE_STATE_PATH = fileURLToPath(
new URL('../../../../test-data/display_layout_with_child_overlay_plot.json', import.meta.url)
);
const TINY_IMAGE_BASE64 = const TINY_IMAGE_BASE64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII'; 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
test.describe('Display Layout Sub-object Actions @localStorage', () => {
const INIT_ITC_START_BOUNDS = '2024-11-12 19:11:11.000Z';
const INIT_ITC_END_BOUNDS = '2024-11-12 20:11:11.000Z';
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
test.use({
storageState: CHILD_PLOT_STORAGE_STATE_PATH
});
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.getByLabel('Expand My Items folder').click();
const waitForMyItemsNavigation = page.waitForURL(`**/mine/?*`);
await page
.getByLabel('Main Tree')
.getByLabel('Navigate to Parent Display Layout layout Object')
.click();
// Wait for the URL to change to the display layout
await waitForMyItemsNavigation;
});
test('Open in New Tab action preserves time bounds @2p', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7524'
});
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6982'
});
const TEST_FIXED_START_TIME = 1731352271000; // 2024-11-11 19:11:11.000Z
const TEST_FIXED_END_TIME = TEST_FIXED_START_TIME + 3600000; // 2024-11-11 20:11:11.000Z
// Verify the ITC has the expected initial bounds
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('Start bounds')
.textContent()
).toEqual(INIT_ITC_START_BOUNDS);
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('End bounds')
.textContent()
).toEqual(INIT_ITC_END_BOUNDS);
// Update the global fixed bounds to 2024-11-11 19:11:11.000Z / 2024-11-11 20:11:11.000Z
const url = page.url().split('?')[0];
await navigateToObjectWithFixedTimeBounds(
page,
url,
TEST_FIXED_START_TIME,
TEST_FIXED_END_TIME
);
// ITC bounds should still match the initial ITC bounds
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('Start bounds')
.textContent()
).toEqual(INIT_ITC_START_BOUNDS);
expect(
await page
.getByLabel('Child Overlay Plot 1 Frame Controls')
.getByLabel('End bounds')
.textContent()
).toEqual(INIT_ITC_END_BOUNDS);
// Open the Child Overlay Plot 1 in a new tab
await page.getByLabel('View menu items').click();
const pagePromise = page.context().waitForEvent('page');
await page.getByLabel('Open In New Tab').click();
const newPage = await pagePromise;
await newPage.waitForLoadState('domcontentloaded');
// Verify that the global time conductor bounds in the new page match the updated global bounds
expect(
await newPage.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
).toEqual(NEW_GLOBAL_START_BOUNDS);
expect(
await newPage.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
).toEqual(NEW_GLOBAL_END_BOUNDS);
// Verify that the ITC is enabled in the new page
await expect(newPage.getByLabel('Disable Independent Time Conductor')).toBeVisible();
// Verify that the ITC bounds in the new page match the original ITC bounds
expect(
await newPage
.getByLabel('Independent Time Conductor Panel')
.getByLabel('Start bounds')
.textContent()
).toEqual(INIT_ITC_START_BOUNDS);
expect(
await newPage
.getByLabel('Independent Time Conductor Panel')
.getByLabel('End bounds')
.textContent()
).toEqual(INIT_ITC_END_BOUNDS);
});
});
test.describe('Display Layout Toolbar Actions @localStorage', () => { test.describe('Display Layout Toolbar Actions @localStorage', () => {
const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout'; const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout';
const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1'; const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1';
@ -50,7 +159,7 @@ test.describe('Display Layout Toolbar Actions @localStorage', () => {
await page.getByLabel('Edit Object').click(); await page.getByLabel('Edit Object').click();
}); });
test.use({ test.use({
storageState: LOCALSTORAGE_PATH storageState: CHILD_LAYOUT_STORAGE_STATE_PATH
}); });
test('can add/remove Text element to a single layout', async ({ page }) => { test('can add/remove Text element to a single layout', async ({ page }) => {
@ -336,7 +445,7 @@ test.describe('Display Layout', () => {
const startDate = '2021-12-30 01:01:00.000Z'; const startDate = '2021-12-30 01:01:00.000Z';
const endDate = '2021-12-30 01:11:00.000Z'; const endDate = '2021-12-30 01:11:00.000Z';
await setIndependentTimeConductorBounds(page, startDate, endDate); await setIndependentTimeConductorBounds(page, { start: startDate, end: endDate });
// check image date // check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();

View File

@ -248,11 +248,10 @@ test.describe('Flexible Layout', () => {
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// flip on independent time conductor // flip on independent time conductor
await setIndependentTimeConductorBounds( await setIndependentTimeConductorBounds(page, {
page, start: '2021-12-30 01:01:00.000Z',
'2021-12-30 01:01:00.000Z', end: '2021-12-30 01:11:00.000Z'
'2021-12-30 01:11:00.000Z' });
);
// check image date // check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible(); await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();

View File

@ -175,13 +175,13 @@ test.describe('Gauge', () => {
}); });
// Try to create a Folder into the Gauge. Should be disallowed. // Try to create a Folder into the Gauge. Should be disallowed.
await page.getByRole('button', { name: /Create/ }).click(); await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('menuitem', { name: /Folder/ }).click(); await page.getByRole('menuitem', { name: /Folder/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await page.getByLabel('Cancel').click(); await page.getByLabel('Cancel').click();
// Try to create a Display Layout into the Gauge. Should be disallowed. // Try to create a Display Layout into the Gauge. Should be disallowed.
await page.getByRole('button', { name: /Create/ }).click(); await page.getByRole('button', { name: 'Create' }).click();
await page.getByRole('menuitem', { name: /Display Layout/ }).click(); await page.getByRole('menuitem', { name: /Display Layout/ }).click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
}); });

View File

@ -37,6 +37,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
}); });
test('Can click on telemetry and see data in inspector @2p', async ({ page, context }) => { test('Can click on telemetry and see data in inspector @2p', async ({ page, context }) => {
const initStartBounds = await page.getByLabel('Start bounds').textContent();
const initEndBounds = await page.getByLabel('End bounds').textContent();
const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, { const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, {
type: 'Example Data Visualization Source' type: 'Example Data Visualization Source'
}); });
@ -78,5 +80,9 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
await newPage.waitForLoadState(); await newPage.waitForLoadState();
// expect new tab title to contain 'Second Sine Wave Generator' // expect new tab title to contain 'Second Sine Wave Generator'
await expect(newPage).toHaveTitle('Second Sine Wave Generator'); await expect(newPage).toHaveTitle('Second Sine Wave Generator');
// Verify that "Open in New Tab" preserves the time bounds
expect(initStartBounds).toEqual(await newPage.getByLabel('Start bounds').textContent());
expect(initEndBounds).toEqual(await newPage.getByLabel('End bounds').textContent());
}); });
}); });

View File

@ -83,7 +83,7 @@ test.describe('Snapshot Container tests', () => {
// name: "Dropped Overlay Plot" // name: "Dropped Overlay Plot"
// }); // });
await page.getByLabel('Take a Notebook Snapshot').click(); await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click(); await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await page.getByLabel('Show Snapshots').click(); await page.getByLabel('Show Snapshots').click();
}); });

View File

@ -114,7 +114,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultFrameBorderColor), hexToRGB(defaultFrameBorderColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2. Note: https://github.com/nasa/openmct/issues/7337 // Check styles on StackedPlot2. Note: https://github.com/nasa/openmct/issues/7337
@ -122,7 +124,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultFrameBorderColor), hexToRGB(defaultFrameBorderColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
}); });
@ -143,7 +147,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(defaultTextColor), hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2 // Check styles on StackedPlot2
@ -151,7 +157,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(defaultTextColor), hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2 // Set styles using setStyles function on StackedPlot1 but not StackedPlot2
@ -160,7 +168,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor, setBorderColor,
setBackgroundColor, setBackgroundColor,
setTextColor, setTextColor,
page.getByLabel('StackedPlot1 Frame') page.getByRole('group', { name: 'StackedPlot1 Frame' })
); );
// Check styles on StackedPlot1 // Check styles on StackedPlot1
@ -168,7 +176,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2 // Check styles on StackedPlot2
@ -176,7 +186,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(defaultTextColor), hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Save Flexible Layout // Save Flexible Layout
@ -191,7 +203,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2 // Check styles on StackedPlot2
@ -199,7 +213,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(defaultTextColor), hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
}); });
@ -241,7 +257,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2 to verify they are the default // Check styles on StackedPlot2 to verify they are the default
@ -249,7 +267,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(defaultTextColor), hexToRGB(defaultTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Set styles using setStyles function on StackedPlot2 // Set styles using setStyles function on StackedPlot2
@ -258,7 +278,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor, setBorderColor,
setBackgroundColor, setBackgroundColor,
setTextColor, setTextColor,
page.getByLabel('StackedPlot2 Frame') page.getByRole('group', { name: 'StackedPlot2 Frame' })
); );
// Check styles on StackedPlot2 // Check styles on StackedPlot2
@ -266,7 +286,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Save Flexible Layout // Save Flexible Layout
@ -281,7 +303,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2 // Check styles on StackedPlot2
@ -289,7 +313,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Directly navigate to the flexible layout // Directly navigate to the flexible layout
@ -326,7 +352,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Check styles on StackedPlot2 matches previous set colors // Check styles on StackedPlot2 matches previous set colors
@ -334,7 +362,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot2 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
}); });
@ -356,7 +386,7 @@ test.describe('Flexible Layout styling', () => {
setBorderColor, setBorderColor,
setBackgroundColor, setBackgroundColor,
setTextColor, setTextColor,
page.getByLabel('StackedPlot1 Frame') page.getByRole('group', { name: 'StackedPlot1 Frame' })
); );
// Check styles using checkStyles function // Check styles using checkStyles function
@ -364,7 +394,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(setBorderColor), hexToRGB(setBorderColor),
hexToRGB(setBackgroundColor), hexToRGB(setBackgroundColor),
hexToRGB(setTextColor), hexToRGB(setTextColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Save Flexible Layout // Save Flexible Layout
@ -386,7 +418,7 @@ test.describe('Flexible Layout styling', () => {
'No Style', 'No Style',
'No Style', 'No Style',
'No Style', 'No Style',
page.getByLabel('StackedPlot1 Frame') page.getByRole('group', { name: 'StackedPlot1 Frame' })
); );
// Check styles using checkStyles function // Check styles using checkStyles function
@ -394,7 +426,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(inheritedColor), hexToRGB(inheritedColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
// Save Flexible Layout // Save Flexible Layout
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
@ -408,7 +442,9 @@ test.describe('Flexible Layout styling', () => {
hexToRGB(defaultBorderTargetColor), hexToRGB(defaultBorderTargetColor),
NO_STYLE_RGBA, NO_STYLE_RGBA,
hexToRGB(inheritedColor), hexToRGB(inheritedColor),
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target') page
.getByRole('group', { name: 'StackedPlot1 Frame' })
.getByLabel('Stacked Plot Style Target')
); );
}); });

View File

@ -67,7 +67,7 @@ test.describe('Style Inspector Options', () => {
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible(); await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();
// Select Stacked Layout Column // Select Stacked Layout Column
await page.getByLabel('Stacked Plot Frame').click(); await page.getByRole('group', { name: 'Stacked Plot Frame' }).click();
// The overall Flex Layout or Stacked Plot itself MUST be style-able. // The overall Flex Layout or Stacked Plot itself MUST be style-able.
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible(); await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();

View File

@ -178,7 +178,7 @@ test.describe('Performance tests', () => {
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming)); console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
// Click Close Icon // Click Close Icon
await page.locator('[aria-label="Close"]').click(); await page.getByRole('button', { name: 'Close' }).click();
await page.evaluate(() => window.performance.mark('view-large-close-button')); await page.evaluate(() => window.performance.mark('view-large-close-button'));
//await client.send('HeapProfiler.enable'); //await client.send('HeapProfiler.enable');

View File

@ -69,14 +69,14 @@ test.describe('Visual - Header @a11y', () => {
}); });
test('show snapshot button', async ({ page, theme }) => { test('show snapshot button', async ({ page, theme }) => {
await page.getByLabel('Take a Notebook Snapshot').click(); await page.getByLabel('Open the Notebook Snapshot Menu').click();
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click(); await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, { await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, {
scope: header scope: header
}); });
await expect(await page.getByLabel('Show Snapshots')).toBeVisible(); await expect(page.getByLabel('Show Snapshots')).toBeVisible();
}); });
}); });

View File

@ -91,7 +91,7 @@ test.describe('Flexible Layout styling @a11y', () => {
setBorderColor, setBorderColor,
setBackgroundColor, setBackgroundColor,
setTextColor, setTextColor,
page.getByLabel('StackedPlot1 Frame') page.getByRole('group', { name: 'StackedPlot1 Frame' })
); );
await percySnapshot( await percySnapshot(

View File

@ -53,11 +53,11 @@ test.describe('Visual - Telemetry Views', () => {
await page.goto(telemetry.url, { waitUntil: 'domcontentloaded' }); await page.goto(telemetry.url, { waitUntil: 'domcontentloaded' });
//Click this button to see telemetry display options //Click this button to see telemetry display options
await page.getByRole('button', { name: 'Plot' }).click(); await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Telemetry Table').click(); await page.getByLabel('Telemetry Table').click();
//Get Table View in place //Get Table View in place
expect(await page.getByLabel('Expand Columns')).toBeInViewport(); await expect(page.getByLabel('Expand Columns')).toBeInViewport();
await percySnapshot(page, `Default Telemetry Table View (theme: ${theme})`); await percySnapshot(page, `Default Telemetry Table View (theme: ${theme})`);

View File

@ -55,6 +55,7 @@
</template> </template>
<script> <script>
const ONE_HOUR = 60 * 60 * 1000;
export default { export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data() { data() {
@ -77,6 +78,10 @@ export default {
selectItem(item, event) { selectItem(item, event) {
event.stopPropagation(); event.stopPropagation();
const bounds = this.openmct.time.getBounds(); const bounds = this.openmct.time.getBounds();
const otherBounds = {
start: bounds.start - ONE_HOUR,
end: bounds.end + ONE_HOUR
};
const selection = [ const selection = [
{ {
element: this.$el, element: this.$el,
@ -88,6 +93,9 @@ export default {
icon: item.type.cssClass icon: item.type.cssClass
}, },
dataRanges: [ dataRanges: [
{
bounds: otherBounds
},
{ {
bounds bounds
} }

View File

@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />

View File

@ -29,10 +29,13 @@
:key="action.name" :key="action.name"
role="menuitem" role="menuitem"
:aria-disabled="action.isDisabled" :aria-disabled="action.isDisabled"
:class="action.cssClass"
:aria-label="action.name" :aria-label="action.name"
aria-describedby="item-description"
:class="action.cssClass"
:title="action.description" :title="action.description"
@click="action.onItemClicked" @click="action.onItemClicked"
@mouseover="toggleItem(action)"
@mouseleave="toggleItem()"
> >
{{ action.name }} {{ action.name }}
</li> </li>
@ -52,16 +55,23 @@
v-for="action in options.actions" v-for="action in options.actions"
:key="action.name" :key="action.name"
role="menuitem" role="menuitem"
aria-describedby="item-description"
:aria-disabled="action.isDisabled" :aria-disabled="action.isDisabled"
:class="action.cssClass" :class="action.cssClass"
:aria-label="action.name" :aria-label="action.name"
:title="action.description" :title="action.description"
@click="action.onItemClicked" @click="action.onItemClicked"
@mouseover="toggleItem(action)"
@mouseleave="toggleItem()"
> >
{{ action.name }} {{ action.name }}
</li> </li>
<li v-if="options.actions.length === 0">No actions defined.</li> <li v-if="options.actions.length === 0">No actions defined.</li>
</ul> </ul>
<div v-if="hoveredItem" id="item-description" class="visually-hidden" aria-live="polite">
<span v-if="hoveredItem.name">{{ hoveredItem.name }}</span>
<span v-if="hoveredItem.description">: {{ hoveredItem.description }}</span>
</div>
</div> </div>
</template> </template>
@ -70,11 +80,21 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
export default { export default {
mixins: [popupMenuMixin], mixins: [popupMenuMixin],
inject: ['options'], inject: ['options'],
data() {
return {
hoveredItem: null
};
},
computed: { computed: {
optionsLabel() { optionsLabel() {
const label = this.options.label ? `${this.options.label} Menu` : 'Menu'; const label = this.options.label ? `${this.options.label} Context Menu` : 'Context Menu';
return label; return label;
} }
},
methods: {
toggleItem(action) {
this.hoveredItem = action ?? null;
}
} }
}; };
</script> </script>

View File

@ -38,8 +38,8 @@
:key="action.name" :key="action.name"
role="menuitem" role="menuitem"
:aria-disabled="action.isDisabled" :aria-disabled="action.isDisabled"
aria-describedby="item-description"
:class="action.cssClass" :class="action.cssClass"
:title="action.description"
@click="action.onItemClicked" @click="action.onItemClicked"
@mouseover="toggleItemDescription(action)" @mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()" @mouseleave="toggleItemDescription()"
@ -64,7 +64,7 @@
role="menuitem" role="menuitem"
:class="action.cssClass" :class="action.cssClass"
:aria-label="action.name" :aria-label="action.name"
:title="action.description" aria-describedby="item-description"
@click="action.onItemClicked" @click="action.onItemClicked"
@mouseover="toggleItemDescription(action)" @mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()" @mouseleave="toggleItemDescription()"
@ -74,13 +74,13 @@
<li v-if="options.actions.length === 0">No actions defined.</li> <li v-if="options.actions.length === 0">No actions defined.</li>
</ul> </ul>
<div class="c-super-menu__item-description"> <div aria-live="polite" class="c-super-menu__item-description">
<div :class="['l-item-description__icon', 'bg-' + hoveredItem.cssClass]"></div> <div :class="itemDescriptionIconClass"></div>
<div class="l-item-description__name"> <div class="l-item-description__name">
{{ hoveredItem.name }} {{ hoveredItemName }}
</div> </div>
<div class="l-item-description__description"> <div id="item-description" class="l-item-description__description">
{{ hoveredItem.description }} {{ hoveredItemDescription }}
</div> </div>
</div> </div>
</div> </div>
@ -90,26 +90,39 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
export default { export default {
mixins: [popupMenuMixin], mixins: [popupMenuMixin],
inject: ['options'], inject: ['options'],
data: function () { data() {
return { return {
hoveredItem: {} hoveredItem: null
}; };
}, },
computed: { computed: {
optionsLabel() { optionsLabel() {
const label = this.options.label ? `${this.options.label} Super Menu` : 'Super Menu'; const label = this.options.label ? `${this.options.label} Super Menu` : 'Super Menu';
return label; return label;
},
itemDescriptionIconClass() {
const iconClass = ['l-item-description__icon'];
if (this.hoveredItem) {
iconClass.push('bg-' + this.hoveredItem.cssClass);
}
return iconClass;
},
hoveredItemName() {
return this.hoveredItem?.name ?? '';
},
hoveredItemDescription() {
return this.hoveredItem?.description ?? '';
} }
}, },
methods: { methods: {
toggleItemDescription(action = {}) { toggleItemDescription(action = null) {
const hoveredItem = { const hoveredItem = {
name: action.name, name: action?.name,
description: action.description, description: action?.description,
cssClass: action.cssClass cssClass: action?.cssClass
}; };
this.hoveredItem = Object.assign({}, this.hoveredItem, hoveredItem); this.hoveredItem = hoveredItem;
} }
} }
}; };

View File

@ -58,7 +58,7 @@
const CONTEXT_MENU_ACTIONS = ['viewDatumAction', 'viewHistoricalData', 'remove']; const CONTEXT_MENU_ACTIONS = ['viewDatumAction', 'viewHistoricalData', 'remove'];
const BLANK_VALUE = '---'; const BLANK_VALUE = '---';
import identifierToString from '/src/tools/url.js'; import { objectPathToUrl } from '/src/tools/url.js';
import PreviewAction from '@/ui/preview/PreviewAction.js'; import PreviewAction from '@/ui/preview/PreviewAction.js';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js'; import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
@ -260,7 +260,7 @@ export default {
event.preventDefault(); event.preventDefault();
this.preview(this.objectPath); this.preview(this.objectPath);
} else { } else {
const resultUrl = identifierToString(this.openmct, this.objectPath); const resultUrl = objectPathToUrl(this.openmct, this.objectPath);
this.openmct.router.navigate(resultUrl); this.openmct.router.navigate(resultUrl);
} }
}, },

View File

@ -25,6 +25,7 @@
aria-label="Clock Indicator" aria-label="Clock Indicator"
class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable" class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"
role="complementary" role="complementary"
aria-live="off"
> >
<span class="label c-indicator__label"> <span class="label c-indicator__label">
{{ timeTextValue }} {{ timeTextValue }}

View File

@ -25,7 +25,18 @@
<div class="c-inspect-properties"> <div class="c-inspect-properties">
<div class="c-inspect-properties__header">Numeric Data</div> <div class="c-inspect-properties__header">Numeric Data</div>
</div> </div>
<div ref="numericDataView"></div> <div ref="numericDataView">
<TelemetryFrame
v-for="plotObject of plotObjects"
:key="plotObject.identifier.key"
:bounds="bounds"
:telemetry-object="plotObject"
:path="[plotObject]"
:render-when-visible="plotObject.renderWhenVisible"
>
<Plot />
</TelemetryFrame>
</div>
<div v-if="!hasNumericData"> <div v-if="!hasNumericData">
{{ noNumericDataText }} {{ noNumericDataText }}
@ -33,13 +44,15 @@
</div> </div>
</template> </template>
<script> <script>
import mount from 'utils/mount';
import VisibilityObserver from '../../utils/visibility/VisibilityObserver.js'; import VisibilityObserver from '../../utils/visibility/VisibilityObserver.js';
import Plot from '../plot/PlotView.vue'; import Plot from '../plot/PlotView.vue';
import TelemetryFrame from './TelemetryFrame.vue'; import TelemetryFrame from './TelemetryFrame.vue';
export default { export default {
components: {
TelemetryFrame,
Plot
},
inject: ['openmct', 'domainObject', 'timeFormatter'], inject: ['openmct', 'domainObject', 'timeFormatter'],
props: { props: {
bounds: { bounds: {
@ -90,16 +103,19 @@ export default {
this.clearPlots(); this.clearPlots();
this.unregisterTimeContextList = []; this.unregisterTimeContextList = [];
this.componentsList = [];
this.elementsList = [];
this.visibilityObservers = []; this.visibilityObservers = [];
this.telemetryKeys.forEach(async (telemetryKey) => { this.telemetryKeys.forEach(async (telemetryKey) => {
const plotObject = await this.openmct.objects.get(telemetryKey); const plotObject = await this.openmct.objects.get(telemetryKey);
const visibilityObserver = new VisibilityObserver(
this.$refs.numericDataView,
this.openmct.element
);
plotObject.renderWhenVisible = visibilityObserver.renderWhenVisible;
this.visibilityObservers.push(visibilityObserver);
this.plotObjects.push(plotObject); this.plotObjects.push(plotObject);
this.unregisterTimeContextList.push(this.setIndependentTimeContextForComponent(plotObject)); this.unregisterTimeContextList.push(this.setIndependentTimeContextForComponent(plotObject));
this.renderPlot(plotObject);
}); });
}, },
setIndependentTimeContextForComponent(plotObject) { setIndependentTimeContextForComponent(plotObject) {
@ -110,63 +126,14 @@ export default {
// set the time context of the object to the selected time range // set the time context of the object to the selected time range
return this.openmct.time.addIndependentContext(keyString, this.bounds); return this.openmct.time.addIndependentContext(keyString, this.bounds);
}, },
renderPlot(plotObject) {
const wrapper = document.createElement('div');
const visibilityObserver = new VisibilityObserver(wrapper, this.openmct.element);
const { destroy } = mount(
{
components: {
TelemetryFrame,
Plot
},
provide: {
openmct: this.openmct,
path: [plotObject],
renderWhenVisible: visibilityObserver.renderWhenVisible
},
data() {
return {
plotObject,
bounds: this.bounds
};
},
template: `<TelemetryFrame
:bounds="bounds"
:telemetry-object="plotObject"
>
<Plot />
</TelemetryFrame>`
},
{
app: this.openmct.app,
element: wrapper
}
);
this.componentsList.push(destroy);
this.elementsList.push(wrapper);
this.visibilityObservers.push(visibilityObserver);
this.$refs.numericDataView.append(wrapper);
},
clearPlots() { clearPlots() {
if (this.componentsList?.length) {
this.componentsList.forEach((destroy) => destroy());
delete this.componentsList;
}
if (this.elementsList?.length) {
this.elementsList.forEach((element) => element.remove());
delete this.elementsList;
}
if (this.visibilityObservers?.length) { if (this.visibilityObservers?.length) {
this.visibilityObservers.forEach((visibilityObserver) => visibilityObserver.destroy()); this.visibilityObservers.forEach((visibilityObserver) => visibilityObserver.destroy());
delete this.visibilityObservers; delete this.visibilityObservers;
} }
if (this.plotObjects?.length) { if (this.plotObjects?.length) {
this.plotObjects = []; this.plotObjects.splice(0, this.plotObjects.length);
} }
if (this.unregisterTimeContextList?.length) { if (this.unregisterTimeContextList?.length) {

View File

@ -70,7 +70,9 @@ export default {
inject: ['openmct'], inject: ['openmct'],
provide() { provide() {
return { return {
domainObject: this.telemetryObject domainObject: this.telemetryObject,
path: this.path,
renderWhenVisible: this.renderWhenVisible
}; };
}, },
props: { props: {
@ -81,6 +83,14 @@ export default {
telemetryObject: { telemetryObject: {
type: Object, type: Object,
default: () => {} default: () => {}
},
path: {
type: Array,
default: () => []
},
renderWhenVisible: {
type: Function,
required: true
} }
}, },
data() { data() {
@ -110,7 +120,10 @@ export default {
'tc.mode': 'fixed' 'tc.mode': 'fixed'
}; };
const newTabAction = this.openmct.actions.getAction('newTab'); const newTabAction = this.openmct.actions.getAction('newTab');
newTabAction.invoke([sourceTelemObject], urlParams); // No view context needed, so pass undefined.
// The urlParams arg will override the global time bounds with the data visualization
// plot bounds.
newTabAction.invoke([sourceTelemObject], undefined, urlParams);
this.showMenu = false; this.showMenu = false;
}, },
previewTelemetry() { previewTelemetry() {

View File

@ -52,7 +52,7 @@
import Moment from 'moment'; import Moment from 'moment';
import mount from 'utils/mount'; import mount from 'utils/mount';
import objectPathToUrl from '@/tools/url'; import { objectPathToUrl } from '@/tools/url';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js'; import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
import ImageExporter from '../../../exporters/ImageExporter.js'; import ImageExporter from '../../../exporters/ImageExporter.js';

View File

@ -23,11 +23,11 @@
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left"> <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button <button
class="c-icon-button c-button--menu icon-camera" class="c-icon-button c-button--menu icon-camera"
aria-label="Take a Notebook Snapshot" :aria-label="snapshotMenuLabel"
title="Take a Notebook Snapshot" :title="snapshotMenuLabel"
@click.stop.prevent="showMenu" @click.stop.prevent="showMenu"
> >
<span title="Take Notebook Snapshot" class="c-icon-button__label"> Snapshot </span> <span class="c-icon-button__label">Snapshot</span>
</button> </button>
</div> </div>
</template> </template>
@ -72,6 +72,11 @@ export default {
notebookTypes: [] notebookTypes: []
}; };
}, },
computed: {
snapshotMenuLabel() {
return 'Open the Notebook Snapshot Menu';
}
},
mounted() { mounted() {
validateNotebookStorageObject(); validateNotebookStorageObject();

View File

@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import objectPathToUrl from '/src/tools/url.js'; import { objectPathToUrl } from '/src/tools/url.js';
export default class OpenInNewTab { export default class OpenInNewTab {
constructor(openmct) { constructor(openmct) {
this.name = 'Open In New Tab'; this.name = 'Open In New Tab';
@ -31,8 +31,26 @@ export default class OpenInNewTab {
this._openmct = openmct; this._openmct = openmct;
} }
invoke(objectPath, urlParams = undefined) {
let url = objectPathToUrl(this._openmct, objectPath, urlParams); /**
* Invokes the "Open in New Tab" action. This will open the object in a new
* browser tab. The URL for the new tab is determined by the current object
* path and any custom time bounds.
*
* @param {import('@/api/objects/ObjectAPI').DomainObject[]} objectPath The current object path
* @param {ViewContext} _view The view context for the object being opened (unused)
* @param {Object<string, string | number>} customUrlParams Provides the ability to override
* the global time conductor bounds. It is an object with the following key/value pairs:
* ```
* {
* 'tc.start': <number>,
* 'tc.end': <number>,
* 'tc.mode': 'fixed' | 'local' | <string>
* }
* ```
*/
invoke(objectPath, _view, customUrlParams) {
const url = objectPathToUrl(this._openmct, objectPath, customUrlParams);
window.open(url); window.open(url);
} }
} }

View File

@ -27,6 +27,7 @@
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode', isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode',
{ 'is-expanded': independentTCEnabled } { 'is-expanded': independentTCEnabled }
]" ]"
aria-label="Independent Time Conductor Panel"
> >
<ToggleSwitch <ToggleSwitch
id="independentTCToggle" id="independentTCToggle"

View File

@ -68,6 +68,18 @@ div {
flex: 1 1 auto; flex: 1 1 auto;
} }
.visually-hidden {
// Provides a way to add accessible text to elements
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
/******************************************************** BROWSER ELEMENTS */ /******************************************************** BROWSER ELEMENTS */
body.desktop { body.desktop {
::-webkit-scrollbar { ::-webkit-scrollbar {
@ -361,7 +373,7 @@ body.desktop .has-local-controls {
} }
} }
[aria-disabled = 'true'], [aria-disabled='true'],
*[disabled], *[disabled],
.disabled { .disabled {
opacity: $controlDisabledOpacity; opacity: $controlDisabledOpacity;
@ -397,7 +409,7 @@ body.desktop .has-local-controls {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
content: ""; content: '';
right: 0; right: 0;
width: $fadeTruncateW * 1.5; width: $fadeTruncateW * 1.5;
z-index: 2; z-index: 2;

View File

@ -24,10 +24,17 @@
* Module defining url handling. * Module defining url handling.
*/ */
function getUrlParams(openmct, customUrlParams = {}) { /**
* Convert the current URL parameters to an array of strings.
* @param {import('../../openmct').OpenMCT} openmct
* @returns {Array<string>} newTabParams
*/
export function paramsToArray(openmct, customUrlParams = {}) {
let urlParams = openmct.router.getParams(); let urlParams = openmct.router.getParams();
Object.entries(customUrlParams).forEach((urlParam) => {
const [key, value] = urlParam; // Merge the custom URL parameters with the current URL parameters.
Object.entries(customUrlParams).forEach((param) => {
const [key, value] = param;
urlParams[key] = value; urlParams[key] = value;
}); });
@ -39,21 +46,7 @@ function getUrlParams(openmct, customUrlParams = {}) {
delete urlParams['tc.endBound']; delete urlParams['tc.endBound'];
} }
return urlParams; return Object.entries(urlParams).map(([key, value]) => `${key}=${value}`);
}
export function paramsToArray(openmct, customUrlParams = {}) {
// parse urlParams from an object to an array.
let urlParams = getUrlParams(openmct, customUrlParams);
let newTabParams = [];
for (let key in urlParams) {
if ({}.hasOwnProperty.call(urlParams, key)) {
let param = `${key}=${urlParams[key]}`;
newTabParams.push(param);
}
}
return newTabParams;
} }
export function identifierToString(openmct, objectPath) { export function identifierToString(openmct, objectPath) {
@ -66,7 +59,7 @@ export function identifierToString(openmct, objectPath) {
* @param {any} customUrlParams * @param {any} customUrlParams
* @returns {string} url * @returns {string} url
*/ */
export default function objectPathToUrl(openmct, objectPath, customUrlParams = {}) { export function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {
let url = identifierToString(openmct, objectPath); let url = identifierToString(openmct, objectPath);
let urlParams = paramsToArray(openmct, customUrlParams); let urlParams = paramsToArray(openmct, customUrlParams);

View File

@ -1,5 +1,5 @@
import { createOpenMct, resetApplicationState } from '../utils/testing.js'; import { createOpenMct, resetApplicationState } from '../utils/testing.js';
import { default as objectPathToUrl, identifierToString, paramsToArray } from './url.js'; import { identifierToString, objectPathToUrl, paramsToArray } from './url.js';
describe('the url tool', function () { describe('the url tool', function () {
let openmct; let openmct;

View File

@ -32,6 +32,7 @@
'has-complex-content': complexContent 'has-complex-content': complexContent
} }
]" ]"
:aria-label="ariaLabel"
> >
<div class="c-so-view__header"> <div class="c-so-view__header">
<div class="c-object-label" :class="[statusClass]"> <div class="c-object-label" :class="[statusClass]">
@ -58,6 +59,7 @@
'c-so-view__frame-controls--no-frame': !hasFrame, 'c-so-view__frame-controls--no-frame': !hasFrame,
'has-complex-content': complexContent 'has-complex-content': complexContent
}" }"
:aria-label="`${ariaLabel} Controls`"
> >
<div v-if="supportsIndependentTime" class="c-conductor-holder--compact"> <div v-if="supportsIndependentTime" class="c-conductor-holder--compact">
<independent-time-conductor :domain-object="domainObject" :object-path="objectPath" /> <independent-time-conductor :domain-object="domainObject" :object-path="objectPath" />
@ -163,6 +165,9 @@ export default {
}; };
}, },
computed: { computed: {
ariaLabel() {
return `${this.domainObject.name} Frame`;
},
statusClass() { statusClass() {
return this.status ? `is-status--${this.status}` : ''; return this.status ? `is-status--${this.status}` : '';
} }

View File

@ -145,7 +145,11 @@
:show-edit-view="true" :show-edit-view="true"
@change-action-collection="setActionCollection" @change-action-collection="setActionCollection"
/> />
<component :is="conductorComponent" class="l-shell__time-conductor" /> <component
:is="conductorComponent"
class="l-shell__time-conductor"
aria-label="Global Time Conductor"
/>
</pane> </pane>
<pane <pane
class="l-shell__pane-inspector l-pane--holds-multipane" class="l-shell__pane-inspector l-pane--holds-multipane"

View File

@ -24,9 +24,10 @@
<button <button
class="c-create-button c-button--menu c-button--major icon-plus" class="c-create-button c-button--menu c-button--major icon-plus"
:aria-disabled="isEditing" :aria-disabled="isEditing"
aria-labelledby="create-button-label"
@click.prevent.stop="showCreateMenu" @click.prevent.stop="showCreateMenu"
> >
<span class="c-button__label">Create</span> <span id="create-button-label" class="c-button__label">Create</span>
</button> </button>
</div> </div>
</template> </template>

View File

@ -27,7 +27,8 @@
<button <button
class="c-icon-button c-button--menu" class="c-icon-button c-button--menu"
:class="currentView.cssClass" :class="currentView.cssClass"
title="Change the current view" :title="viewSwitcherLabel"
:aria-label="viewSwitcherLabel"
@click.prevent.stop="showMenu" @click.prevent.stop="showMenu"
> >
<span class="c-icon-button__label"> <span class="c-icon-button__label">
@ -51,6 +52,11 @@ export default {
} }
}, },
emits: ['set-view'], emits: ['set-view'],
computed: {
viewSwitcherLabel() {
return 'Open the View Switcher Menu';
}
},
methods: { methods: {
setView(view) { setView(view) {
this.$emit('set-view', view); this.$emit('set-view', view);

View File

@ -55,7 +55,7 @@
<script> <script>
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js'; import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
import identifierToString from '../../../tools/url.js'; import { objectPathToUrl } from '../../../tools/url.js';
import ObjectPath from '../../components/ObjectPath.vue'; import ObjectPath from '../../components/ObjectPath.vue';
import PreviewAction from '../../preview/PreviewAction.js'; import PreviewAction from '../../preview/PreviewAction.js';
@ -101,7 +101,7 @@ export default {
event.preventDefault(); event.preventDefault();
this.preview(objectPath); this.preview(objectPath);
} else { } else {
let resultUrl = identifierToString(this.openmct, objectPath); let resultUrl = objectPathToUrl(this.openmct, objectPath);
// Remove the vestigial 'ROOT' identifier from url if it exists // Remove the vestigial 'ROOT' identifier from url if it exists
if (resultUrl.includes('/ROOT')) { if (resultUrl.includes('/ROOT')) {

View File

@ -61,7 +61,8 @@ export default {
let sortedActions = this.openmct.actions._groupAndSortActions(actions); let sortedActions = this.openmct.actions._groupAndSortActions(actions);
const menuOptions = { const menuOptions = {
onDestroy: this.onContextMenuDestroyed onDestroy: this.onContextMenuDestroyed,
label: this.objectPath[0].name
}; };
const menuItems = this.openmct.menus.actionsToMenuItems( const menuItems = this.openmct.menus.actionsToMenuItems(

View File

@ -1,4 +1,4 @@
import objectPathToUrl from '../../tools/url.js'; import { objectPathToUrl } from '../../tools/url.js';
export default { export default {
inject: ['openmct'], inject: ['openmct'],