Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
34 changes: 33 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,33 @@ describe("surface shortcuts", () => {
});
});

describe("surface shortcut typing contexts", () => {
// Selector-aware stub: closest() answers only tokens the combined selector
// would actually match, mirroring how the browser resolves it.
const makeTarget = (matches: string | null) => ({
closest(selectors: string) {
if (matches === null || !selectors.includes(matches)) return null;
return {};
},
});

it("treats form fields and every editable region as typing contexts", () => {
expect(surfaceShortcutTargetsTypingContext(makeTarget("input"))).toBe(true);
expect(surfaceShortcutTargetsTypingContext(makeTarget("textarea"))).toBe(true);
expect(surfaceShortcutTargetsTypingContext(makeTarget("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.
// The :not clause sees past contenteditable="false" islands to an editable
// host around them, so nested editors stay protected too.
expect(surfaceShortcutTargetsTypingContext(makeTarget("[contenteditable]"))).toBe(true);
});

it("claims letters when focus sits outside any editable region", () => {
expect(surfaceShortcutTargetsTypingContext(null)).toBe(false);
expect(surfaceShortcutTargetsTypingContext(makeTarget(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
25 changes: 18 additions & 7 deletions apps/web/src/components/RightPanelTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,23 @@ 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. The `:not` clause lets `closest` see past
* non-editable islands (`contenteditable="false"`) to an editable host around
* them, matching ComposerPendingUserInputPanel's typing guard.
*/
export function surfaceShortcutTargetsTypingContext(
target: { closest(selectors: string): unknown } | null,
): boolean {
return (
target?.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])') !=
null
);
}
Comment thread
Lucenx9 marked this conversation as resolved.

function DisabledReasonTooltip(props: { reason: string; trigger: ReactElement }) {
return (
<Tooltip>
Expand Down Expand Up @@ -329,13 +346,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