feat(extension): hover candidate pool

This commit is contained in:
Ljy-0827
2026-08-07 12:02:28 +08:00
parent 6c610faed9
commit eeab97bcdd
5 changed files with 391 additions and 56 deletions
@@ -77,6 +77,19 @@ describe("evaluateHoverTrigger", () => {
expect(decision.eligible).toBe(false);
});
it("rejects labelled dropdown list items as hover triggers", () => {
const decision = evaluateHoverTrigger({
tag: "li",
label: "文档C+D",
attrs: { class: "dropdown-item", tabindex: "0" },
rect: { x: 820, y: 48, w: 160, h: 32 },
cursor: "pointer",
pointerEvents: "auto",
});
expect(decision.eligible).toBe(false);
});
it("rejects ordinary controls without hover popup signals", () => {
const decision = evaluateHoverTrigger({
tag: "button",
+16 -4
View File
@@ -26,6 +26,7 @@ export interface HoverTriggerDecision {
const HOVER_TRIGGER_MIN_SCORE = 45;
const HOVER_POPUP_SIGNAL_RE =
/(menu|dropdown|popover|popup|avatar|profile|account|user|more|ellipsis|caret)/;
const HOVER_TEXT_ITEM_RE = /(^|[\s_-])(dropdown-item|menu-item|context-menu-item|option)($|[\s_-])/;
function attr(signals: HoverTriggerSignals, name: string): string | undefined {
return signals.attrs?.[name.toLowerCase()];
@@ -59,6 +60,14 @@ export function hasHoverPopupSignal(signals: HoverTriggerSignals): boolean {
return HOVER_POPUP_SIGNAL_RE.test(haystack);
}
export function hasStrongHoverExpansionSignal(signals: HoverTriggerSignals): boolean {
return (
signals.cssHoverMatch === true ||
attr(signals, "aria-haspopup") !== undefined ||
(attr(signals, "aria-expanded") ?? "").toLowerCase() === "false"
);
}
export function hasDirectHoverInteractiveSignal(signals: HoverTriggerSignals): boolean {
const tag = signals.tag.toLowerCase();
const role = (signals.role ?? "").toLowerCase();
@@ -104,7 +113,12 @@ export function evaluateHoverTrigger(signals: HoverTriggerSignals): HoverTrigger
const directInteractive = hasDirectHoverInteractiveSignal(signals);
const popupSignal = hasHoverPopupSignal(signals);
const isTextNavigationItem =
(tag === "a" || role === "link" || role.startsWith("menuitem")) &&
(tag === "a" ||
tag === "li" ||
role === "link" ||
role === "option" ||
role.startsWith("menuitem") ||
HOVER_TEXT_ITEM_RE.test(attr(signals, "class") ?? "")) &&
!graphic &&
!!label &&
label.toLowerCase() !== "image";
@@ -118,9 +132,7 @@ export function evaluateHoverTrigger(signals: HoverTriggerSignals): HoverTrigger
const surfaceTriggerEvidence =
(popupSignal && !isTextNavigationItem) ||
compactTopbarIcon ||
signals.cssHoverMatch === true ||
attr(signals, "aria-haspopup") !== undefined ||
(attr(signals, "aria-expanded") ?? "").toLowerCase() === "false";
hasStrongHoverExpansionSignal(signals);
if (!directInteractive || !surfaceTriggerEvidence) {
return { eligible: false, score: 0, reasons: [] };
}