mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(ui): close composer interaction gaps
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
handleChatComposerDropdownShow,
|
||||
markPointerOpenedChatComposerDropdown,
|
||||
restorePointerOpenedChatComposerTrigger,
|
||||
} from "./chat-picker-overlay.ts";
|
||||
|
||||
describe("chat picker overlay", () => {
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
});
|
||||
|
||||
it("does not restore pointer focus after keyboard input takes over", () => {
|
||||
const dropdown = document.createElement("wa-dropdown");
|
||||
const trigger = document.createElement("button");
|
||||
trigger.slot = "trigger";
|
||||
dropdown.append(trigger);
|
||||
document.body.append(dropdown);
|
||||
|
||||
dropdown.addEventListener("wa-show", handleChatComposerDropdownShow);
|
||||
dropdown.dispatchEvent(new Event("wa-show"));
|
||||
dropdown.addEventListener("pointerdown", markPointerOpenedChatComposerDropdown);
|
||||
trigger.dispatchEvent(new Event("pointerdown", { bubbles: true, composed: true }));
|
||||
|
||||
trigger.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { bubbles: true, composed: true, key: "Enter" }),
|
||||
);
|
||||
|
||||
dropdown.addEventListener("wa-after-show", restorePointerOpenedChatComposerTrigger);
|
||||
dropdown.dispatchEvent(new Event("wa-after-show"));
|
||||
|
||||
expect(trigger.hasAttribute("data-chat-pointer-restored-focus")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -87,7 +87,10 @@ function ensureChatComposerPickerDismissal(): void {
|
||||
(node): node is HTMLElement =>
|
||||
node instanceof HTMLElement && node.localName === "wa-dropdown",
|
||||
);
|
||||
dropdown?.removeAttribute(POINTER_OPENED_PICKER_ATTRIBUTE);
|
||||
if (dropdown) {
|
||||
pointerOpenedDropdowns.delete(dropdown);
|
||||
dropdown.removeAttribute(POINTER_OPENED_PICKER_ATTRIBUTE);
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -7,12 +7,17 @@ const dictationHarness = vi.hoisted(() => ({
|
||||
onCommit: (transcript: string) => void;
|
||||
onTap: () => void;
|
||||
},
|
||||
controllers: [] as Array<{ active: boolean; handlePointerDown: () => void }>,
|
||||
controllers: [] as Array<{
|
||||
active: boolean;
|
||||
locksComposer: boolean;
|
||||
handlePointerDown: () => void;
|
||||
}>,
|
||||
}));
|
||||
|
||||
vi.mock("../chat/composer-dictation.ts", () => ({
|
||||
ComposerDictationController: class {
|
||||
active = false;
|
||||
locksComposer = false;
|
||||
|
||||
constructor(options: { onCommit: (transcript: string) => void; onTap: () => void }) {
|
||||
dictationHarness.options = options;
|
||||
@@ -133,4 +138,28 @@ describe("NewSessionDictationControl", () => {
|
||||
expect(insertTranscript).not.toHaveBeenCalled();
|
||||
expect(onMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("publishes whether dictation currently locks draft submission", () => {
|
||||
const control = new NewSessionDictationControl({
|
||||
textarea: { captureSelection: vi.fn(), insertTranscript: vi.fn() } as never,
|
||||
getClient: () => ({}) as never,
|
||||
isConnected: () => true,
|
||||
canCommit: () => true,
|
||||
onMessage: vi.fn(),
|
||||
onError: vi.fn(),
|
||||
onClearError: vi.fn(),
|
||||
requestUpdate: vi.fn(),
|
||||
});
|
||||
|
||||
control.render("agent-a");
|
||||
expect(control.locked).toBe(false);
|
||||
|
||||
const controller = dictationHarness.controllers[0];
|
||||
if (!controller) {
|
||||
throw new Error("expected dictation controller");
|
||||
}
|
||||
controller.locksComposer = true;
|
||||
|
||||
expect(control.locked).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +38,10 @@ export class NewSessionDictationControl {
|
||||
this.devicePicker = new ComposerMicrophonePicker(options.requestUpdate);
|
||||
}
|
||||
|
||||
get locked(): boolean {
|
||||
return this.dictation?.locksComposer === true;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.owner = null;
|
||||
this.dictation?.dispose();
|
||||
|
||||
@@ -537,6 +537,8 @@ export class NewSessionPage extends OpenClawLightDomElement {
|
||||
const worktreeNameInvalid =
|
||||
this.place.worktree && !isWorktreeNameValid(this.place.worktreeName);
|
||||
const capabilities = this.submission.capabilities;
|
||||
const voiceControl = this.dictation.render(this.routeOwnerKey());
|
||||
const dictationLocked = this.dictation.locked;
|
||||
return html`
|
||||
<div class="new-session-page__draft" aria-busy=${String(this.submission.submitting)}>
|
||||
${this.renderTargetBar()}
|
||||
@@ -561,7 +563,7 @@ export class NewSessionPage extends OpenClawLightDomElement {
|
||||
agent: this.place.selectedAgent(),
|
||||
agentId: this.place.agentId,
|
||||
attachmentDraft: this.submission.attachmentDraft,
|
||||
canSubmit: !this.submission.submitting && this.submission.canSubmit(),
|
||||
canSubmit: !this.submission.submitting && !dictationLocked && this.submission.canSubmit(),
|
||||
submitDisabledReason: this.submission.submitDisabledReason(),
|
||||
blockedSubmitNotice: this.submission.blockedSubmitNotice(),
|
||||
context: this.context,
|
||||
@@ -589,11 +591,14 @@ export class NewSessionPage extends OpenClawLightDomElement {
|
||||
requestUpdate: () => this.requestUpdate(),
|
||||
submitting: this.submission.submitting,
|
||||
textareaController: this.submission.composerTextarea,
|
||||
voiceControl: this.dictation.render(this.routeOwnerKey()),
|
||||
voiceControl,
|
||||
messageLocked: Boolean(this.submission.pendingPlacement.sessionKey),
|
||||
terminalAction: this.submission.showStartInTerminal()
|
||||
? {
|
||||
canStart: !this.submission.submitting && this.submission.canSubmit("terminal"),
|
||||
canStart:
|
||||
!this.submission.submitting &&
|
||||
!dictationLocked &&
|
||||
this.submission.canSubmit("terminal"),
|
||||
disabledReason: this.submission.submitBlock("terminal")?.reason,
|
||||
onStart: () => void this.submission.startInTerminal(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user