feat(#7394): Incorporate Status Indicators into the main Vue app (#7395)

* feat(IndicatorAPI): accept Vue components

- Adds a new property to Indicators, `component`, which is a synchronous or asynchronous Vue component.
- Adds `wrapHtmlElement` utility function to create anonymous Vue components out of `HTMLElement`s (for backwards compatibility)
- Refactors StatusIndicators.vue to use dynamic components, allowing us to dynamically render indicators (and keep it all within Vue's ecosystem).

* refactor(indicators): use dynamic Vue components instead of `mount()`

- Refactors some indicators to use Vue components directly as async components

* refactor: use Vue reactivity for timestamps in clock indicator

* fix(test): fix unit tests and remove some console logs

* test(e2e): stabilize ladSet e2e test

* test: mix in some Vue indicators in indicatorSpec

* refactor: cleanup variable names

* docs: update IndicatorAPI docs

* fix(e2e): wait for async status bar components to load before snapshot

* a11y(e2e): add aria-labels and wait for status bar to load

* test(e2e): add exact: true

* fix: initializing indicators

* fix(typo): uhhh.. how did that get there? O_o

* fix: use synchronous components for default indicators

* test: clean up, remove unnecessary `nextTick()`s

* test: remove more `nextTick()`s

* refactor: lint:fix

* fix: `on` -> `off`

* test(e2e): stabilize tabs test

* test(e2e): attempt to stabilize limit lines tests with `toHaveCount()` assertion
This commit is contained in:
Jesse Mazzella 2024-01-23 15:15:22 -08:00 committed by GitHub
parent 4cf63062c0
commit 114864429a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 202 additions and 158 deletions

View File

@ -260,9 +260,9 @@ async function assertLimitLinesExistAndAreVisible(page) {
await waitForPlotsToRender(page);
// Wait for limit lines to be created
await page.waitForSelector('.js-limit-area', { state: 'attached' });
const limitLineCount = await page.locator('.c-plot-limit-line').count();
// There should be 10 limit lines created by default
expect(await page.locator('.c-plot-limit-line').count()).toBe(10);
await expect(page.locator('.c-plot-limit-line')).toHaveCount(10);
const limitLineCount = await page.locator('.c-plot-limit-line').count();
for (let i = 0; i < limitLineCount; i++) {
await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible();
}

View File

@ -55,7 +55,7 @@ test.describe('Tabs View', () => {
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
// no canvas (i.e., sine wave generator) in the document should be visible
await expect(page.locator('canvas')).toBeHidden();
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
// select second tab
await page.getByLabel(`${notebook.name} tab`).click();
@ -64,7 +64,7 @@ test.describe('Tabs View', () => {
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
// no canvas (i.e., sine wave generator) in the document should be visible
await expect(page.locator('canvas')).toBeHidden();
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
// select third tab
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
@ -83,6 +83,6 @@ test.describe('Tabs View', () => {
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
// no canvas (i.e., sine wave generator) in the document should be visible
await expect(page.locator('canvas')).toBeHidden();
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
});
});

View File

@ -99,7 +99,7 @@ test.describe('Grand Search', () => {
page.waitForNavigation(),
page.getByLabel('OpenMCT Search').getByText('Clock A').click()
]);
await expect(page.getByRole('status', { name: 'Clock' })).toBeVisible();
await expect(page.getByRole('status', { name: 'Clock', exact: true })).toBeVisible();
await grandSearchInput.fill('Disp');
await expect(page.getByLabel('Object Search Result').first()).toContainText(

View File

@ -36,6 +36,22 @@ test.describe('Visual - Header @a11y', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL and Hide Tree
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
// Wait for status bar to load
await expect(
page.getByRole('status', {
name: 'Clock Indicator'
})
).toBeInViewport();
await expect(
page.getByRole('status', {
name: 'Global Clear Indicator'
})
).toBeInViewport();
await expect(
page.getByRole('status', {
name: 'Snapshot Indicator'
})
).toBeInViewport();
});
test('header sizing', async ({ page, theme }) => {

View File

@ -23,7 +23,7 @@ import percySnapshot from '@percy/playwright';
import { fileURLToPath } from 'url';
import * as utils from '../../helper/faultUtils.js';
import { test } from '../../pluginFixtures.js';
import { expect, test } from '../../pluginFixtures.js';
test.describe('Fault Management Visual Tests', () => {
test('icon test', async ({ page, theme }) => {
@ -32,6 +32,23 @@ test.describe('Fault Management Visual Tests', () => {
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Wait for status bar to load
await expect(
page.getByRole('status', {
name: 'Clock Indicator'
})
).toBeInViewport();
await expect(
page.getByRole('status', {
name: 'Global Clear Indicator'
})
).toBeInViewport();
await expect(
page.getByRole('status', {
name: 'Snapshot Indicator'
})
).toBeInViewport();
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
});

View File

@ -22,9 +22,12 @@
import EventEmitter from 'EventEmitter';
import vueWrapHtmlElement from '../../utils/vueWrapHtmlElement.js';
import SimpleIndicator from './SimpleIndicator.js';
class IndicatorAPI extends EventEmitter {
/** @type {import('../../../openmct.js').OpenMCT} */
openmct;
constructor(openmct) {
super();
@ -42,6 +45,18 @@ class IndicatorAPI extends EventEmitter {
return new SimpleIndicator(this.openmct);
}
/**
* @typedef {import('vue').Component} VueComponent
*/
/**
* @typedef {Object} Indicator
* @property {HTMLElement} [element]
* @property {VueComponent|Promise<VueComponent>} [vueComponent]
* @property {string} key
* @property {number} priority
*/
/**
* Accepts an indicator object, which is a simple object
* with a two attributes: 'element' which has an HTMLElement
@ -62,11 +77,20 @@ class IndicatorAPI extends EventEmitter {
* myIndicator.text("Hello World!");
* myIndicator.iconClass("icon-info");
*
* If you would like to use a Vue component, you can pass it in
* directly as the 'vueComponent' attribute of the indicator object.
* This accepts a Vue component or a promise that resolves to a Vue component (for asynchronous
* rendering).
*
* @param {Indicator} indicator
*/
add(indicator) {
if (!indicator.priority) {
indicator.priority = this.openmct.priority.DEFAULT;
}
if (!indicator.vueComponent) {
indicator.vueComponent = vueWrapHtmlElement(indicator.element);
}
this.indicatorObjects.push(indicator);

View File

@ -19,6 +19,8 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { defineComponent } from 'vue';
import { createOpenMct, resetApplicationState } from '../../utils/testing.js';
import SimpleIndicator from './SimpleIndicator.js';
@ -33,7 +35,7 @@ describe('The Indicator API', () => {
return resetApplicationState(openmct);
});
function generateIndicator(className, label, priority) {
function generateHTMLIndicator(className, label, priority) {
const element = document.createElement('div');
element.classList.add(className);
const textNode = document.createTextNode(label);
@ -46,8 +48,25 @@ describe('The Indicator API', () => {
return testIndicator;
}
it('can register an indicator', () => {
const testIndicator = generateIndicator('test-indicator', 'This is a test indicator', 2);
function generateVueIndicator(priority) {
return {
vueComponent: defineComponent({
template: '<div class="test-indicator">This is a test indicator</div>'
}),
priority
};
}
it('can register an HTML indicator', () => {
const testIndicator = generateHTMLIndicator('test-indicator', 'This is a test indicator', 2);
openmct.indicators.add(testIndicator);
expect(openmct.indicators.indicatorObjects).toBeDefined();
// notifier indicator is installed by default
expect(openmct.indicators.indicatorObjects.length).toBe(2);
});
it('can register a Vue indicator', () => {
const testIndicator = generateVueIndicator(2);
openmct.indicators.add(testIndicator);
expect(openmct.indicators.indicatorObjects).toBeDefined();
// notifier indicator is installed by default
@ -55,37 +74,40 @@ describe('The Indicator API', () => {
});
it('can order indicators based on priority', () => {
const testIndicator1 = generateIndicator(
const testIndicator1 = generateHTMLIndicator(
'test-indicator-1',
'This is a test indicator',
openmct.priority.LOW
);
openmct.indicators.add(testIndicator1);
const testIndicator2 = generateIndicator(
const testIndicator2 = generateHTMLIndicator(
'test-indicator-2',
'This is another test indicator',
openmct.priority.DEFAULT
);
openmct.indicators.add(testIndicator2);
const testIndicator3 = generateIndicator(
const testIndicator3 = generateHTMLIndicator(
'test-indicator-3',
'This is yet another test indicator',
openmct.priority.LOW
);
openmct.indicators.add(testIndicator3);
const testIndicator4 = generateIndicator(
const testIndicator4 = generateHTMLIndicator(
'test-indicator-4',
'This is yet another test indicator',
openmct.priority.HIGH
);
openmct.indicators.add(testIndicator4);
expect(openmct.indicators.indicatorObjects.length).toBe(5);
const testIndicator5 = generateVueIndicator(openmct.priority.DEFAULT);
openmct.indicators.add(testIndicator5);
expect(openmct.indicators.indicatorObjects.length).toBe(6);
const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority();
expect(indicatorObjectsByPriority.length).toBe(5);
expect(indicatorObjectsByPriority.length).toBe(6);
expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT);
});

View File

@ -20,7 +20,10 @@
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-indicator c-indicator--clickable icon-clear-data s-status-caution">
<div
aria-label="Global Clear Indicator"
class="c-indicator c-indicator--clickable icon-clear-data s-status-caution"
>
<span class="label c-indicator__label">
<button @click="globalClearEmit">Clear Data</button>
</span>

View File

@ -19,8 +19,6 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import mount from 'utils/mount';
import ClearDataAction from './ClearDataAction.js';
import GlobalClearIndicator from './components/GlobalClearIndicator.vue';
@ -31,27 +29,10 @@ export default function plugin(appliesToObjects, options = { indicator: true })
return function install(openmct) {
if (installIndicator) {
const { vNode, destroy } = mount(
{
components: {
GlobalClearIndicator
},
provide: {
openmct
},
template: '<GlobalClearIndicator></GlobalClearIndicator>'
},
{
app: openmct.app,
element: document.createElement('div')
}
);
let indicator = {
element: vNode.el,
vueComponent: GlobalClearIndicator,
key: 'global-clear-indicator',
priority: openmct.priority.DEFAULT,
destroy: destroy
priority: openmct.priority.DEFAULT
};
openmct.indicators.add(indicator);

View File

@ -21,7 +21,6 @@
*****************************************************************************/
import { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing';
import { nextTick } from 'vue';
import ClearDataPlugin from './plugin.js';
@ -208,12 +207,11 @@ describe('The Clear Data Plugin:', () => {
it('installs', () => {
const globalClearIndicator = openmct.indicators.indicatorObjects.find(
(indicator) => indicator.key === 'global-clear-indicator'
).element;
).vueComponent;
expect(globalClearIndicator).toBeDefined();
});
it('renders its major elements', async () => {
await nextTick();
it('renders its major elements', () => {
const indicatorClass = appHolder.querySelector('.c-indicator');
const iconClass = appHolder.querySelector('.icon-clear-data');
const indicatorLabel = appHolder.querySelector('.c-indicator__label');
@ -228,10 +226,7 @@ describe('The Clear Data Plugin:', () => {
const indicatorLabel = appHolder.querySelector('.c-indicator__label');
const buttonElement = indicatorLabel.querySelector('button');
const clickEvent = createMouseEvent('click');
openmct.objectViews.on('clearData', () => {
// when we click the button, this event should fire
done();
});
openmct.objectViews.on('clearData', done);
buttonElement.dispatchEvent(clickEvent);
});
});

View File

@ -22,6 +22,7 @@
<template>
<div
aria-label="Clock Indicator"
class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"
role="complementary"
>
@ -40,27 +41,32 @@ export default {
props: {
indicatorFormat: {
type: String,
required: true
default: 'YYYY/MM/DD HH:mm:ss'
}
},
data() {
return {
timeTextValue: this.openmct.time.getClock() ? this.openmct.time.now() : undefined
timestamp: this.openmct.time.getClock() ? this.openmct.time.now() : undefined
};
},
computed: {
timeTextValue() {
return `${moment.utc(this.timestamp).format(this.indicatorFormat)} ${
this.openmct.time.getTimeSystem().name
}`;
}
},
mounted() {
this.tick = raf(this.tick);
this.openmct.time.on('tick', this.tick);
this.tick(this.timeTextValue);
this.tick(this.timestamp);
},
beforeUnmount() {
this.openmct.time.off('tick', this.tick);
},
methods: {
tick(timestamp) {
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} ${
this.openmct.time.getTimeSystem().name
}`;
this.timestamp = timestamp;
}
}
};

View File

@ -21,14 +21,12 @@
*****************************************************************************/
import momentTimezone from 'moment-timezone';
import mount from 'utils/mount';
import ClockViewProvider from './ClockViewProvider.js';
import ClockIndicator from './components/ClockIndicator.vue';
export default function ClockPlugin(options) {
return function install(openmct) {
const CLOCK_INDICATOR_FORMAT = 'YYYY/MM/DD HH:mm:ss';
openmct.types.addType('clock', {
name: 'Clock',
description:
@ -92,31 +90,9 @@ export default function ClockPlugin(options) {
});
openmct.objectViews.addProvider(new ClockViewProvider(openmct));
if (options && options.enableClockIndicator === true) {
const element = document.createElement('div');
const { vNode } = mount(
{
components: {
ClockIndicator
},
provide: {
openmct
},
data() {
return {
indicatorFormat: CLOCK_INDICATOR_FORMAT
};
},
template: '<ClockIndicator :indicator-format="indicatorFormat" />'
},
{
app: openmct.app,
element
}
);
if (options?.enableClockIndicator === true) {
const indicator = {
element: vNode.el,
vueComponent: ClockIndicator,
key: 'clock-indicator',
priority: openmct.priority.LOW
};

View File

@ -195,10 +195,6 @@ describe('Clock plugin:', () => {
let clockIndicator;
afterEach(() => {
if (clockIndicator) {
clockIndicator.remove();
}
clockIndicator = undefined;
if (appHolder) {
appHolder.remove();
@ -223,7 +219,7 @@ describe('Clock plugin:', () => {
clockIndicator = openmct.indicators.indicatorObjects.find(
(indicator) => indicator.key === 'clock-indicator'
).element;
).vueComponent;
const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined;
expect(hasClockIndicator).toBe(true);
@ -231,14 +227,16 @@ describe('Clock plugin:', () => {
it('contains text', async () => {
await setupClock(true);
await nextTick();
await new Promise((resolve) => requestAnimationFrame(resolve));
clockIndicator = openmct.indicators.indicatorObjects.find(
(indicator) => indicator.key === 'clock-indicator'
).element;
const clockIndicatorText = clockIndicator.textContent.trim();
).vueComponent;
const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined;
expect(hasClockIndicator).toBe(true);
const clockIndicatorText = appHolder
.querySelector('.t-indicator-clock .c-indicator__label')
.textContent.trim();
const textIncludesUTC = clockIndicatorText.includes('UTC');
expect(textIncludesUTC).toBe(true);

View File

@ -106,7 +106,6 @@ describe('the plugin', function () {
flexibleView.show(child, false);
await nextTick();
console.log(child);
const flexTitle = child.querySelector('.c-fl');
expect(flexTitle).not.toBeNull();

View File

@ -48,11 +48,12 @@
import mount from 'utils/mount';
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants.js';
import { getSnapshotContainer } from '../plugin.js';
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container.js';
import SnapshotContainerComponent from './NotebookSnapshotContainer.vue';
export default {
inject: ['openmct', 'snapshotContainer'],
inject: ['openmct'],
data() {
return {
expanded: false,
@ -62,6 +63,9 @@ export default {
flashIndicator: false
};
},
created() {
this.snapshotContainer = getSnapshotContainer(this.openmct);
},
mounted() {
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
this.updateSnapshotIndicatorTitle();

View File

@ -20,8 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import mount from 'utils/mount';
import { notebookImageMigration } from '../notebook/utils/notebook-migration.js';
import CopyToNotebookAction from './actions/CopyToNotebookAction.js';
import ExportNotebookAsTextAction from './actions/ExportNotebookAsTextAction.js';
@ -39,7 +37,7 @@ import NotebookViewProvider from './NotebookViewProvider.js';
import SnapshotContainer from './snapshot-container.js';
let notebookSnapshotContainer;
function getSnapshotContainer(openmct) {
export function getSnapshotContainer(openmct) {
if (!notebookSnapshotContainer) {
notebookSnapshotContainer = new SnapshotContainer(openmct);
}
@ -66,7 +64,6 @@ function installBaseNotebookFunctionality(openmct) {
return;
}
const snapshotContainer = getSnapshotContainer(openmct);
const notebookSnapshotImageType = {
name: 'Notebook Snapshot Image Storage',
description: 'Notebook Snapshot Image Storage object',
@ -82,27 +79,10 @@ function installBaseNotebookFunctionality(openmct) {
openmct.actions.register(new CopyToNotebookAction(openmct));
openmct.actions.register(new ExportNotebookAsTextAction(openmct));
const { vNode, destroy } = mount(
{
components: {
NotebookSnapshotIndicator
},
provide: {
openmct,
snapshotContainer
},
template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
},
{
app: openmct.app
}
);
const indicator = {
element: vNode.el,
vueComponent: NotebookSnapshotIndicator,
key: 'notebook-snapshot-indicator',
priority: openmct.priority.DEFAULT,
destroy: destroy
priority: openmct.priority.DEFAULT
};
openmct.indicators.add(indicator);

View File

@ -336,24 +336,23 @@ describe('Notebook plugin:', () => {
let snapshotIndicator;
let drawerElement;
function clickSnapshotIndicator() {
const indicator = element.querySelector('.icon-camera');
const button = indicator.querySelector('button');
async function clickSnapshotIndicator() {
const button =
appHolder.querySelector('[aria-label="Show Snapshots"]') ??
appHolder.querySelector('[aria-label="Hide Snapshots"]');
const clickEvent = createMouseEvent('click');
button.dispatchEvent(clickEvent);
await nextTick();
}
beforeEach(() => {
beforeEach(async () => {
snapshotIndicator = openmct.indicators.indicatorObjects.find(
(indicator) => indicator.key === 'notebook-snapshot-indicator'
).element;
).vueComponent;
element.append(snapshotIndicator);
return nextTick().then(() => {
drawerElement = document.querySelector('.l-shell__drawer');
});
await nextTick();
drawerElement = document.querySelector('.l-shell__drawer');
});
afterEach(() => {
@ -361,7 +360,6 @@ describe('Notebook plugin:', () => {
drawerElement.classList.remove('is-expanded');
}
snapshotIndicator.remove();
snapshotIndicator = undefined;
if (drawerElement) {
@ -375,11 +373,11 @@ describe('Notebook plugin:', () => {
expect(hasSnapshotIndicator).toBe(true);
});
it('snapshots container has class isExpanded', () => {
it('snapshots container has class isExpanded', async () => {
let classes = drawerElement.classList;
const isExpandedBefore = classes.contains('is-expanded');
clickSnapshotIndicator();
await clickSnapshotIndicator();
classes = drawerElement.classList;
const isExpandedAfterFirstClick = classes.contains('is-expanded');
@ -387,15 +385,15 @@ describe('Notebook plugin:', () => {
expect(isExpandedAfterFirstClick).toBeTrue();
});
it('snapshots container does not have class isExpanded', () => {
it('snapshots container does not have class isExpanded', async () => {
let classes = drawerElement.classList;
const isExpandedBefore = classes.contains('is-expanded');
clickSnapshotIndicator();
await clickSnapshotIndicator();
classes = drawerElement.classList;
const isExpandedAfterFirstClick = classes.contains('is-expanded');
clickSnapshotIndicator();
await clickSnapshotIndicator();
classes = drawerElement.classList;
const isExpandedAfterSecondClick = classes.contains('is-expanded');
@ -404,8 +402,8 @@ describe('Notebook plugin:', () => {
expect(isExpandedAfterSecondClick).toBeFalse();
});
it('show notebook snapshots container text', () => {
clickSnapshotIndicator();
it('show notebook snapshots container text', async () => {
await clickSnapshotIndicator();
const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name');
const snapshotsText = notebookSnapshots.textContent.trim();

View File

@ -72,8 +72,8 @@ export default {
this.openmct.notifications.on('dismiss-all', this.updateNotifications);
},
unmounted() {
this.openmct.notifications.of('notification', this.updateNotifications);
this.openmct.notifications.of('dismiss-all', this.updateNotifications);
this.openmct.notifications.off('notification', this.updateNotifications);
this.openmct.notifications.off('dismiss-all', this.updateNotifications);
},
methods: {
dismissAllNotifications() {

View File

@ -19,32 +19,14 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import mount from 'utils/mount';
import NotificationIndicator from './components/NotificationIndicator.vue';
export default function plugin() {
return function install(openmct) {
const { vNode, destroy } = mount(
{
components: {
NotificationIndicator
},
provide: {
openmct
},
template: '<NotificationIndicator></NotificationIndicator>'
},
{
app: openmct.app
}
);
let indicator = {
key: 'notifications-indicator',
element: vNode.el,
priority: openmct.priority.DEFAULT,
destroy: destroy
vueComponent: NotificationIndicator,
priority: openmct.priority.DEFAULT
};
openmct.indicators.add(indicator);
};

View File

@ -37,7 +37,7 @@
>
<CreateButton class="l-shell__create-button" />
<GrandSearch ref="grand-search" />
<StatusIndicators class="l-shell__head-section l-shell__indicators" />
<StatusIndicators />
<button
class="l-shell__head__collapse-button c-icon-button"
:class="

View File

@ -17,24 +17,43 @@
at runtime from the About dialog for additional information.
-->
<template>
<div></div>
<div class="l-shell__head-section l-shell__indicators">
<component
:is="indicator.value.vueComponent"
v-for="indicator in sortedIndicators"
:key="indicator.value.key"
role="status"
/>
</div>
</template>
<script>
import { shallowRef } from 'vue';
export default {
inject: ['openmct'],
data() {
return {
indicators: this.openmct.indicators.getIndicatorObjectsByPriority().map(shallowRef)
};
},
computed: {
sortedIndicators() {
if (this.indicators.length === 0) {
return [];
}
return [...this.indicators].sort((a, b) => b.value.priority - a.value.priority);
}
},
beforeUnmount() {
this.openmct.indicators.off('addIndicator', this.addIndicator);
},
mounted() {
this.openmct.indicators.getIndicatorObjectsByPriority().forEach(this.addIndicator);
created() {
this.openmct.indicators.on('addIndicator', this.addIndicator);
},
methods: {
addIndicator(indicator) {
this.$el.appendChild(indicator.element);
this.indicators.push(shallowRef(indicator));
}
}
};

View File

@ -0,0 +1,24 @@
import { defineComponent, h, onMounted, ref } from 'vue';
/**
* Compatibility wrapper for wrapping an HTMLElement in a Vue component.
*
* @param {HTMLElement} element
* @returns {import('vue').Component}
*/
export default function vueWrapHtmlElement(element) {
return defineComponent({
setup() {
const wrapper = ref(null);
onMounted(() => {
if (wrapper.value) {
wrapper.value.appendChild(element);
}
});
// Render function returning the wrapper div
return () => h('div', { ref: wrapper });
}
});
}