Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion apps/web/src/components/RightPanelTabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import type { DesktopPreviewFavicon, PreviewSessionSnapshot } from "@t3tools/con
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vite-plus/test";

import { RightPanelTabs, surfaceShortcutActionForKey, tabMuteMenuItem } from "./RightPanelTabs";
import {
RightPanelTabs,
surfaceShortcutActionForKey,
surfaceShortcutTargetsTypingContext,
tabMuteMenuItem,
} from "./RightPanelTabs";

function shortcutEvent(
key: string,
Expand Down Expand Up @@ -166,6 +171,26 @@ describe("surface shortcuts", () => {
});
});

describe("surface shortcut typing contexts", () => {
const targetIn = (selector: string | null) => ({
closest: (selectors: string) => (selector === null ? null : { matches: selector }),
});
Comment thread
Lucenx9 marked this conversation as resolved.
Resolved

it("treats form fields and every contenteditable as typing contexts", () => {
expect(surfaceShortcutTargetsTypingContext(targetIn("input"))).toBe(true);
expect(surfaceShortcutTargetsTypingContext(targetIn("textarea"))).toBe(true);
expect(surfaceShortcutTargetsTypingContext(targetIn("select"))).toBe(true);
// The chat composer is a contenteditable that sits empty until a draft
// exists; launcher letters claimed from it redirected prompts into shells.
expect(surfaceShortcutTargetsTypingContext(targetIn("[contenteditable]"))).toBe(true);
});

it("claims letters when focus sits outside any editable", () => {
expect(surfaceShortcutTargetsTypingContext(targetIn(null))).toBe(false);
expect(surfaceShortcutTargetsTypingContext(null)).toBe(false);
});
});

describe("RightPanelTabs audio indicator", () => {
// A muted tab only shows the indicator while it is actually making sound:
// arming mute on a quiet tab is deliberate and stays invisible until there
Expand Down
20 changes: 13 additions & 7 deletions apps/web/src/components/RightPanelTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,18 @@ export function surfaceShortcutActionForKey<
);
}

/**
* A focused editable is a typing context whether or not it has text yet: an
* empty chat composer at rest is still where the user's next keystrokes are
* meant to land, and claiming launcher letters from it would redirect prompts
* into whatever surface opens.
*/
export function surfaceShortcutTargetsTypingContext(
target: { closest(selectors: string): unknown } | null,
): boolean {
return target?.closest("input, textarea, select, [contenteditable]") != null;
}
Comment thread
Lucenx9 marked this conversation as resolved.
Resolved

function DisabledReasonTooltip(props: { reason: string; trigger: ReactElement }) {
return (
<Tooltip>
Expand Down Expand Up @@ -329,13 +341,7 @@ function RightPanelEmptyState(props: {
if (!action) return;
if (document.querySelector(LAUNCHER_SHORTCUT_BLOCKING_LAYERS)) return;
const target = event.target;
if (target instanceof HTMLElement) {
if (target.closest("input, textarea, select")) return;
// An empty contenteditable (the chat composer at rest) does not
// count as typing; letters only become text once a draft exists.
const editable = target.isContentEditable ? target : target.closest("[contenteditable]");
if (editable && (editable.textContent ?? "").trim().length > 0) return;
}
if (target instanceof Element && surfaceShortcutTargetsTypingContext(target)) return;
event.preventDefault();
event.stopPropagation();
action.onClick();
Expand Down
Loading