Prevent model tooltip preview flicker

This commit is contained in:
Dariusz L
2026-09-14 00:42:48 +02:00
parent f714c055a6
commit b5a2209854
2 changed files with 124 additions and 41 deletions
@@ -908,6 +908,53 @@ test('model tooltip stays hidden while the context menu is visible', () => {
);
});
test('model tooltip waits for its preview before becoming visible', async () => {
const showTooltip = eval(`(${extractMethod(searchPanelMethodsSource, 'showTooltip')})`);
const window = new Window();
const previousDocument = globalThis.document;
const target = window.document.createElement('span');
const tooltipElement = window.document.createElement('div');
let resolveMedia;
target.setAttribute('data-tooltip', 'model.safetensors');
target.setAttribute('data-tooltip-image', '/model-preview?path=model.safetensors');
globalThis.document = window.document;
try {
const dialog = {
contextMenu: { style: { display: 'none' } },
tooltipElement,
normalizeTooltipTarget() {},
async getTooltipPreviewMediaType() {
return 'image';
},
waitForTooltipMedia() {
return new Promise(resolve => {
resolveMedia = resolve;
});
},
positionTooltip() {},
};
const tooltipPromise = showTooltip.call(dialog, target);
await new Promise(resolve => setTimeout(resolve, 0));
assert.equal(tooltipElement.style.display, 'none');
assert.equal(tooltipElement.hasAttribute('data-visible'), false);
assert.ok(tooltipElement.querySelector('.mr-tooltip-preview'));
resolveMedia(true);
await tooltipPromise;
assert.equal(tooltipElement.style.display, 'block');
assert.equal(tooltipElement.getAttribute('data-visible'), 'true');
assert.equal(tooltipElement.classList.contains('mr-global-tooltip-with-image'), true);
} finally {
globalThis.document = previousDocument;
await window.happyDOM.close();
}
});
test('model tooltip revalidates and detects video previews from the preview route', async () => {
const getTooltipPreviewMediaType = eval(`(${extractMethod(searchPanelMethodsSource, 'getTooltipPreviewMediaType')})`);
const previousFetch = globalThis.fetch;
+77 -41
View File
@@ -662,6 +662,47 @@ export const searchPanelMethods = {
}
},
waitForTooltipMedia(media, mediaType, previewUrl) {
if (!media || !previewUrl) return Promise.resolve(false);
return new Promise((resolve) => {
const readyEvent = mediaType === 'video' ? 'loadeddata' : 'load';
let settled = false;
const finish = (loaded) => {
if (settled) return;
settled = true;
media.removeEventListener(readyEvent, onReady);
media.removeEventListener('error', onError);
resolve(loaded);
};
const onReady = () => {
if (mediaType === 'image' && typeof media.decode === 'function') {
media.decode().then(
() => finish(true),
() => finish(false),
);
return;
}
finish(true);
};
const onError = () => finish(false);
media.addEventListener(readyEvent, onReady);
media.addEventListener('error', onError);
try {
media.src = previewUrl;
} catch (_error) {
finish(false);
return;
}
const alreadyReady = mediaType === 'video'
? Number(media.readyState || 0) >= 2
: Boolean(media.complete && Number(media.naturalWidth || 0) > 0);
if (alreadyReady) Promise.resolve().then(onReady);
});
},
async showTooltip(target) {
if (!target || !this.tooltipElement) return;
if (this.contextMenu?.style.display === 'block') {
@@ -680,16 +721,10 @@ export const searchPanelMethods = {
this._tooltipTarget = target;
this.tooltipElement.replaceChildren();
this.tooltipElement.classList.remove('mr-global-tooltip-with-image');
this.tooltipElement.style.display = 'none';
this.tooltipElement.removeAttribute('data-visible');
if (imageUrl) {
const label = document.createElement('div');
label.className = 'mr-tooltip-label';
label.textContent = text;
this.tooltipElement.append(label);
this.tooltipElement.style.display = 'block';
this.positionTooltip(target);
this.tooltipElement.setAttribute('data-visible', 'true');
const mediaType = await this.getTooltipPreviewMediaType(imageUrl);
if (
this._tooltipTarget !== target
@@ -698,41 +733,42 @@ export const searchPanelMethods = {
return;
}
if (!mediaType) {
this.tooltipElement.classList.remove('mr-global-tooltip-with-image');
this.positionTooltip(target);
return;
}
const media = document.createElement(mediaType === 'video' ? 'video' : 'img');
media.className = 'mr-tooltip-preview';
if (mediaType === 'video') {
media.autoplay = true;
media.loop = true;
media.muted = true;
media.playsInline = true;
media.preload = 'metadata';
this.tooltipElement.textContent = text;
} else {
media.alt = '';
media.decoding = 'async';
}
media.addEventListener(mediaType === 'video' ? 'loadeddata' : 'load', () => {
if (this._tooltipTarget === target) {
this.positionTooltip(target);
if (mediaType === 'video') {
media.play?.().catch(() => {});
}
const label = document.createElement('div');
label.className = 'mr-tooltip-label';
label.textContent = text;
const media = document.createElement(mediaType === 'video' ? 'video' : 'img');
media.className = 'mr-tooltip-preview';
if (mediaType === 'video') {
media.autoplay = true;
media.loop = true;
media.muted = true;
media.playsInline = true;
media.preload = 'metadata';
} else {
media.alt = '';
media.decoding = 'async';
}
});
media.addEventListener('error', () => {
if (this._tooltipTarget !== target) return;
media.remove();
this.tooltipElement.classList.remove('mr-global-tooltip-with-image');
this.positionTooltip(target);
});
media.src = imageUrl;
this.tooltipElement.prepend(media);
this.tooltipElement.classList.add('mr-global-tooltip-with-image');
this.tooltipElement.append(label);
this.tooltipElement.prepend(media);
this.tooltipElement.classList.add('mr-global-tooltip-with-image');
const mediaLoaded = await this.waitForTooltipMedia(media, mediaType, imageUrl);
if (
this._tooltipTarget !== target
|| this.contextMenu?.style.display === 'block'
) {
return;
}
if (!mediaLoaded) {
media.remove();
this.tooltipElement.classList.remove('mr-global-tooltip-with-image');
this.tooltipElement.textContent = text;
} else if (mediaType === 'video') {
media.play?.().catch(() => {});
}
}
} else {
this.tooltipElement.textContent = text;
}