Fix nested Yomitan popup focus loss

This commit is contained in:
2026-04-07 21:45:12 -07:00
committed by sudacode
parent 49e46e6b9b
commit 47b26cc4a5
9 changed files with 396 additions and 24 deletions
+43 -3
View File
@@ -3,7 +3,9 @@ import assert from 'node:assert/strict';
import { createRendererRecoveryController } from './error-recovery.js';
import {
YOMITAN_POPUP_HOST_SELECTOR,
YOMITAN_POPUP_IFRAME_SELECTOR,
YOMITAN_POPUP_VISIBLE_HOST_SELECTOR,
hasYomitanPopupIframe,
isYomitanPopupIframe,
isYomitanPopupVisible,
@@ -284,9 +286,25 @@ test('hasYomitanPopupIframe queries for modern + legacy selector', () => {
assert.equal(selector, YOMITAN_POPUP_IFRAME_SELECTOR);
});
test('hasYomitanPopupIframe falls back to popup host selector for shadow-hosted popups', () => {
const selectors: string[] = [];
const root = {
querySelector: (value: string) => {
selectors.push(value);
if (value === YOMITAN_POPUP_HOST_SELECTOR) {
return {};
}
return null;
},
} as unknown as ParentNode;
assert.equal(hasYomitanPopupIframe(root), true);
assert.deepEqual(selectors, [YOMITAN_POPUP_IFRAME_SELECTOR, YOMITAN_POPUP_HOST_SELECTOR]);
});
test('isYomitanPopupVisible requires visible iframe geometry', () => {
const previousWindow = (globalThis as { window?: unknown }).window;
let selector = '';
const selectors: string[] = [];
const visibleFrame = {
getBoundingClientRect: () => ({ width: 320, height: 180 }),
} as unknown as HTMLIFrameElement;
@@ -309,18 +327,40 @@ test('isYomitanPopupVisible requires visible iframe geometry', () => {
try {
const root = {
querySelectorAll: (value: string) => {
selector = value;
selectors.push(value);
if (value === YOMITAN_POPUP_VISIBLE_HOST_SELECTOR || value === YOMITAN_POPUP_HOST_SELECTOR) {
return [];
}
return [hiddenFrame, visibleFrame];
},
} as unknown as ParentNode;
assert.equal(isYomitanPopupVisible(root), true);
assert.equal(selector, YOMITAN_POPUP_IFRAME_SELECTOR);
assert.deepEqual(selectors, [
YOMITAN_POPUP_VISIBLE_HOST_SELECTOR,
YOMITAN_POPUP_IFRAME_SELECTOR,
]);
} finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
}
});
test('isYomitanPopupVisible detects visible shadow-hosted popup marker without iframe access', () => {
let selector = '';
const root = {
querySelectorAll: (value: string) => {
selector = value;
if (value === YOMITAN_POPUP_VISIBLE_HOST_SELECTOR) {
return [{ getAttribute: () => 'true' }];
}
return [];
},
} as unknown as ParentNode;
assert.equal(isYomitanPopupVisible(root), true);
assert.equal(selector, YOMITAN_POPUP_VISIBLE_HOST_SELECTOR);
});
test('scrollActiveRuntimeOptionIntoView scrolls active runtime option with nearest block', () => {
const calls: Array<{ block?: ScrollLogicalPosition }> = [];
const activeItem = {