mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(ui): dismiss session hovercard before its menu opens (#126345)
* fix(ui): dismiss session hovercard before menu * test(ui): keep hovercard regression focused * fix(ui): dismiss catalog session hovercards * test(ui): use typed catalog session key
This commit is contained in:
committed by
GitHub
parent
177828aa75
commit
3aff572d74
@@ -0,0 +1,35 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { SidebarCatalogMenuController } from "./app-sidebar-catalog-menu.ts";
|
||||
import { SESSION_MENU_OPEN_EVENT } from "./session-progress-hovercard-target.ts";
|
||||
|
||||
describe("SidebarCatalogMenuController", () => {
|
||||
it("dismisses the matching hovercard before opening the catalog menu", () => {
|
||||
const trigger = document.createElement("button");
|
||||
const order: string[] = [];
|
||||
trigger.addEventListener(SESSION_MENU_OPEN_EVENT, () => order.push("dismiss"));
|
||||
const controller = new SidebarCatalogMenuController({
|
||||
beforeOpen: () => order.push("open"),
|
||||
requestUpdate: vi.fn(),
|
||||
terminalAvailable: () => true,
|
||||
navigate: vi.fn(),
|
||||
});
|
||||
|
||||
controller.open(
|
||||
{
|
||||
key: { catalogId: "codex", hostId: "gateway:local", threadId: "thread-1" },
|
||||
agentId: "main",
|
||||
routeId: "chat",
|
||||
navigation: {},
|
||||
canOpenTerminal: true,
|
||||
meta: "now",
|
||||
},
|
||||
10,
|
||||
20,
|
||||
trigger,
|
||||
);
|
||||
|
||||
expect(order).toEqual(["dismiss", "open"]);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { openCatalogSessionInTerminal } from "../lib/sessions/catalog-terminal.t
|
||||
import type { CatalogSessionMenuRequest } from "./app-sidebar-session-catalogs.ts";
|
||||
import "./catalog-session-menu.ts";
|
||||
import type { CatalogSessionMenuAction } from "./catalog-session-menu.ts";
|
||||
import { SESSION_MENU_OPEN_EVENT } from "./session-progress-hovercard-target.ts";
|
||||
|
||||
type SidebarCatalogSessionMenuState = CatalogSessionMenuRequest & { x: number; y: number };
|
||||
|
||||
@@ -30,6 +31,9 @@ export class SidebarCatalogMenuController {
|
||||
y: number,
|
||||
trigger: HTMLElement | null = null,
|
||||
): void {
|
||||
trigger?.dispatchEvent(
|
||||
new CustomEvent(SESSION_MENU_OPEN_EVENT, { bubbles: true, composed: true }),
|
||||
);
|
||||
this.hooks.beforeOpen();
|
||||
this.trigger = trigger;
|
||||
this.state = { ...request, x, y };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const SESSION_PROGRESS_HOVER_LINK_SELECTOR = "a.markdown-session-link[data-session-key]";
|
||||
const SESSION_PROGRESS_HOVER_SIDEBAR_SELECTOR = ".sidebar-recent-session[data-session-key]";
|
||||
export const SESSION_MENU_OPEN_EVENT = "openclaw-session-menu-open";
|
||||
export const SESSION_PROGRESS_HOVER_TARGET_SELECTOR = `${SESSION_PROGRESS_HOVER_LINK_SELECTOR}, ${SESSION_PROGRESS_HOVER_SIDEBAR_SELECTOR}`;
|
||||
|
||||
export function sessionProgressHoverTargetFromEvent(event: Event): HTMLElement | null {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { createPortaledHovercard, PortaledHovercardController } from "./portaled
|
||||
import { renderSessionHovercard } from "./session-hovercard.ts";
|
||||
import { SessionLinkTitler } from "./session-link-titling.ts";
|
||||
import {
|
||||
SESSION_MENU_OPEN_EVENT,
|
||||
sessionProgressHoverPlacementForTarget,
|
||||
sessionProgressHoverTargetFromEvent,
|
||||
} from "./session-progress-hovercard-target.ts";
|
||||
@@ -99,6 +100,7 @@ export class SessionProgressHovercardProvider extends ReactiveElement {
|
||||
this.addEventListener("focusin", this.handleFocusIn);
|
||||
this.addEventListener("focusout", this.handleFocusOut);
|
||||
this.addEventListener("keydown", this.handleKeyDown);
|
||||
this.addEventListener(SESSION_MENU_OPEN_EVENT, this.handleSessionMenuOpen);
|
||||
this.sessionLinkTitler.connect();
|
||||
this.connectStore();
|
||||
}
|
||||
@@ -109,6 +111,7 @@ export class SessionProgressHovercardProvider extends ReactiveElement {
|
||||
this.removeEventListener("focusin", this.handleFocusIn);
|
||||
this.removeEventListener("focusout", this.handleFocusOut);
|
||||
this.removeEventListener("keydown", this.handleKeyDown);
|
||||
this.removeEventListener(SESSION_MENU_OPEN_EVENT, this.handleSessionMenuOpen);
|
||||
this.sessionLinkTitler.disconnect();
|
||||
this.disconnectStore();
|
||||
this.close();
|
||||
@@ -209,6 +212,12 @@ export class SessionProgressHovercardProvider extends ReactiveElement {
|
||||
}
|
||||
};
|
||||
|
||||
private readonly handleSessionMenuOpen = (event: Event) => {
|
||||
if (sessionProgressHoverTargetFromEvent(event) === this.activeTarget) {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
private activate(target: HTMLElement, trigger: HTMLElement, delay: number): void {
|
||||
const sessionKey = target.dataset.sessionKey;
|
||||
if (!sessionKey || (target === this.activeTarget && sessionKey === this.activeSessionKey)) {
|
||||
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
SessionOrganizerControllerHost,
|
||||
} from "./session-organizer-controller.ts";
|
||||
import type { SessionOwnerOption } from "./session-owner-chip.ts";
|
||||
import { SESSION_MENU_OPEN_EVENT } from "./session-progress-hovercard-target.ts";
|
||||
|
||||
type SidebarMenuAgent = {
|
||||
id: string;
|
||||
@@ -324,6 +325,9 @@ export class SidebarMenusController implements ReactiveController, SidebarMenusC
|
||||
y: number,
|
||||
trigger: HTMLElement | null = null,
|
||||
) {
|
||||
trigger?.dispatchEvent(
|
||||
new CustomEvent(SESSION_MENU_OPEN_EVENT, { bubbles: true, composed: true }),
|
||||
);
|
||||
if (!this.host.selectedSessionKeys.has(session.key)) {
|
||||
this.host.clearSessionSelection();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import type { Page } from "playwright";
|
||||
import type { Locator, Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { CONTROL_UI_SESSION_PULL_REQUESTS_CHANGED_EVENT } from "../../../src/gateway/control-ui-contract.js";
|
||||
import { SESSION_PULL_REQUESTS_SUBSCRIBE_METHOD } from "../lib/session-pull-requests.ts";
|
||||
@@ -79,6 +79,79 @@ async function emitPullRequestSnapshot(
|
||||
const suite = createChatFlowE2eSuite();
|
||||
|
||||
suite.define(() => {
|
||||
it.each([
|
||||
{
|
||||
open: async (_page: Page, row: Locator) => {
|
||||
await row.locator("[data-session-menu]").click();
|
||||
},
|
||||
source: "More",
|
||||
},
|
||||
{
|
||||
open: async (_page: Page, row: Locator) => {
|
||||
await row.click({ button: "right" });
|
||||
},
|
||||
source: "context menu",
|
||||
},
|
||||
{
|
||||
open: async (page: Page, row: Locator) => {
|
||||
await row.locator(".sidebar-recent-session__link").focus();
|
||||
await page.keyboard.press("Shift+F10");
|
||||
},
|
||||
source: "keyboard",
|
||||
},
|
||||
])("dismisses the session hovercard before opening its menu from $source", async ({ open }) => {
|
||||
const selectedSessionKey = "agent:main:selected-menu";
|
||||
const sessionKey = "agent:main:hovered-menu";
|
||||
|
||||
await suite.withPage(
|
||||
{
|
||||
hasTouch: false,
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
},
|
||||
async ({ page }) => {
|
||||
await installMockGateway(page, {
|
||||
featureMethods: ["chat.metadata", "chat.startup", "progressCard.get"],
|
||||
methodResponses: {
|
||||
"progressCard.get": { card: null },
|
||||
"sessions.list": chatSessionListResponse([
|
||||
{
|
||||
key: selectedSessionKey,
|
||||
kind: "direct",
|
||||
label: "Selected session",
|
||||
updatedAt: 2,
|
||||
},
|
||||
{
|
||||
key: sessionKey,
|
||||
kind: "direct",
|
||||
label: "Hovered session",
|
||||
updatedAt: 1,
|
||||
},
|
||||
]),
|
||||
},
|
||||
sessionKey: selectedSessionKey,
|
||||
});
|
||||
|
||||
await page.goto(controlUiSessionUrl(suite.server.baseUrl, selectedSessionKey));
|
||||
const row = page.locator(`.sidebar-recent-session[data-session-key="${sessionKey}"]`);
|
||||
const trigger = row.locator("[data-session-menu]");
|
||||
const card = page.locator(".session-progress-hovercard");
|
||||
const menu = page.getByRole("menu", { name: "Actions for Hovered session" });
|
||||
await row.waitFor({ state: "visible" });
|
||||
await row.hover();
|
||||
await card.waitFor({ state: "visible" });
|
||||
|
||||
await open(page, row);
|
||||
|
||||
await menu.waitFor({ state: "visible" });
|
||||
await expect.poll(() => card.count()).toBe(0);
|
||||
await expect.poll(() => menu.isVisible()).toBe(true);
|
||||
await expect.poll(() => trigger.getAttribute("aria-expanded")).toBe("true");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("renders safe progress markdown and refreshes the hovered card after a change event", async () => {
|
||||
const now = Date.now();
|
||||
const selectedSessionKey = "agent:main:selected";
|
||||
|
||||
Reference in New Issue
Block a user