Q201: an input method's Enter never picks a mention, and suggestions are announced
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
11369f8fed
commit
ca7721358e
3 files changed
+50
-3
No files matched your search
@@ -698,6 +698,7 @@ export const zh: Record<string, string> = {
|
||||
"Working…": "处理中…",
|
||||
"Waiting for the {name} sign-in in your browser (check {check} of {checks}).": "正在等待你在浏览器中完成 {name} 登录(第 {check}/{checks} 次检查)。",
|
||||
"Stop waiting": "停止等待",
|
||||
"People to mention": "可提及的人",
|
||||
"Layout": "布局",
|
||||
"View": "视图",
|
||||
"Run status": "运行状态",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useRef, useState, type MutableRefObject } from "react";
|
||||
import { t } from "../../i18n/i18n";
|
||||
import { useId, useRef, useState, type MutableRefObject } from "react";
|
||||
import { Avatar } from "../../shared/ui";
|
||||
import type { AgentRecord, EmployeeRecord } from "../../../../shared/domain";
|
||||
import { canonicalMentionHandle } from "../../../../shared/mention-handles";
|
||||
@@ -40,6 +41,7 @@ export function MentionTextarea({
|
||||
}) {
|
||||
const [query, setQuery] = useState<string | null>(null);
|
||||
const [index, setIndex] = useState(0);
|
||||
const listId = useId();
|
||||
const anchor = useRef(0);
|
||||
const innerRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const ref = textareaRef ?? innerRef;
|
||||
@@ -52,6 +54,7 @@ export function MentionTextarea({
|
||||
: [];
|
||||
|
||||
const close = () => setQuery(null);
|
||||
const open = !disabled && query !== null && filtered.length > 0;
|
||||
|
||||
const insertMention = (handle: string) => {
|
||||
const node = ref.current;
|
||||
@@ -81,6 +84,8 @@ export function MentionTextarea({
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// While an input method is composing, Enter and Tab confirm its text; they never pick a person or send.
|
||||
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
||||
if (query !== null && filtered.length > 0) {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
@@ -111,8 +116,8 @@ export function MentionTextarea({
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
{!disabled && query !== null && filtered.length > 0 && (
|
||||
<div style={{
|
||||
{open && (
|
||||
<div id={listId} role="listbox" aria-label={t("People to mention")} style={{
|
||||
position: "absolute",
|
||||
bottom: "100%",
|
||||
left: 0,
|
||||
@@ -129,7 +134,11 @@ export function MentionTextarea({
|
||||
{filtered.map((c, i) => (
|
||||
<button
|
||||
key={c.handle}
|
||||
id={`${listId}-${i}`}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={i === index}
|
||||
tabIndex={-1}
|
||||
onMouseDown={(e) => { e.preventDefault(); insertMention(c.handle); }}
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -158,6 +167,9 @@ export function MentionTextarea({
|
||||
rows={rows}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel}
|
||||
aria-autocomplete="list"
|
||||
aria-controls={open ? listId : undefined}
|
||||
aria-activedescendant={open ? `${listId}-${index}` : undefined}
|
||||
style={{
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -70,3 +70,37 @@ test("a dialog keeps the keyboard inside it, closes on Escape, and gives focus b
|
||||
await closeApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test("mention suggestions are announced, and an input method's Enter never picks a person", async () => {
|
||||
const profileDir = join(tmpdir(), `agentcompany-new-e2e-mention-${randomUUID()}`);
|
||||
mkdirSync(profileDir, { recursive: true });
|
||||
const builtinDir = seedBuiltinSkillsFixture(join(tmpdir(), `agentcompany-new-e2e-mention-builtin-${randomUUID()}`));
|
||||
const app = await electron.launch({
|
||||
args: [resolve("out/main/index.js")],
|
||||
env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir, AGENTCOMPANY_NEW_AUTORUN: "0", AGENTCOMPANY_NEW_BUILTIN_SKILLS_DIR: builtinDir },
|
||||
});
|
||||
try {
|
||||
const page = await app.firstWindow();
|
||||
await onboard(page, "Mention Co");
|
||||
await page.getByRole("button", { name: "Messages" }).click();
|
||||
const composer = page.getByRole("textbox", { name: "Message" });
|
||||
await composer.fill("@");
|
||||
await composer.press("End");
|
||||
await composer.type("c");
|
||||
const list = page.getByRole("listbox", { name: "People to mention" });
|
||||
await expect(list).toBeVisible();
|
||||
const active = await composer.getAttribute("aria-activedescendant");
|
||||
expect(active).toBeTruthy();
|
||||
await expect(list.locator(`[id="${active}"]`)).toHaveAttribute("aria-selected", "true");
|
||||
// An Enter that confirms composed text (isComposing) leaves the words as typed.
|
||||
await composer.evaluate((node) => node.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", isComposing: true, bubbles: true, cancelable: true })));
|
||||
await expect(composer).toHaveValue("@c");
|
||||
await expect(list).toBeVisible();
|
||||
// A plain Enter picks the highlighted person.
|
||||
await composer.press("Enter");
|
||||
await expect(composer).toHaveValue(/^@\S+ $/);
|
||||
await expect(list).toHaveCount(0);
|
||||
} finally {
|
||||
await closeApp(app);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user