diff --git a/docs/channels.md b/docs/channels.md index ad30fe0ebc..c8a4d68d07 100644 --- a/docs/channels.md +++ b/docs/channels.md @@ -1709,7 +1709,8 @@ Escape and Option+Arrow for the shell. conversation is not among the rows, next starts from the top and previous from the bottom. A conversation listed in two sections is stepped over once. The shortcuts are unavailable, leaving the key alone, while the sidebar renders no rows (for - example behind another page's own sidebar). Stepping activates the row's own + example behind another page's own sidebar) or focus is in a text field outside the + composer, which keeps Option+Arrow for editing. Stepping activates the row's own button, so it takes the exact path of a click: the saved selection, navigation intent, session staleness guard and Solo's immediate activation. The list scrolls just far enough to show the row, and nothing above it moves. diff --git a/src/bundled/channels/index.test.tsx b/src/bundled/channels/index.test.tsx index 051314835a..2bd29be279 100644 --- a/src/bundled/channels/index.test.tsx +++ b/src/bundled/channels/index.test.tsx @@ -72,6 +72,20 @@ it("steps only while the sidebar renders rows, and not from a terminal", () => { expect(next.when()).toBe(false); }); +it("stays available in the message composer but not in other text fields", () => { + const next = registered().get("next-conversation"); + sidebar("a", "*b", "c"); + document.body.insertAdjacentHTML( + "beforeend", + ` +
`, + ); + document.querySelector("textarea")?.focus(); + expect(next.when()).toBe(false); + document.querySelector('[role="textbox"]')?.focus(); + expect(next.when()).toBe(true); +}); + it("registers a plain Escape that waits for an open conversation to mark read", () => { const shortcut = registered().get("mark-read"); expect(shortcut).toMatchObject({ diff --git a/src/bundled/channels/index.tsx b/src/bundled/channels/index.tsx index ae54b251ed..449d20246e 100644 --- a/src/bundled/channels/index.tsx +++ b/src/bundled/channels/index.tsx @@ -12,6 +12,7 @@ import { isApplePlatform } from "../../features/shortcuts/format"; import type { ArchiveShortcut, ReadShortcut } from "./ChannelsPage"; import { conversationRows, + inFieldOutsideComposer, inTerminal, stepConversation, } from "./conversation-rows"; @@ -72,8 +73,9 @@ export const apply: PluginModule["apply"] = (ctx) => { when: () => !!archive.current?.available(), run: () => archive.current?.run(), }); - // Slack's sidebar stepping. Terminals keep Option+Arrow, and stepping is - // unavailable (the key is left alone) while the sidebar renders no rows. + // Slack's sidebar stepping. Terminals and text fields outside the composer + // keep Option+Arrow, and stepping is unavailable (the key is left alone) + // while the sidebar renders no rows. const stepping = [ ["previous-conversation", "Previous conversation", "ArrowUp", false, 30], ["next-conversation", "Next conversation", "ArrowDown", false, 31], @@ -99,7 +101,10 @@ export const apply: PluginModule["apply"] = (ctx) => { binding: { key, alt: true, shift: unreadOnly }, order, allowInEditable: true, - when: () => !inTerminal() && conversationRows().length > 0, + when: () => + !inTerminal() && + !inFieldOutsideComposer() && + conversationRows().length > 0, run: () => stepConversation(key === "ArrowUp" ? "previous" : "next", unreadOnly), });