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

describe("surface shortcut typing contexts", () => {
// Selector-aware stub: closest() only answers when the requested selector
// list contains the matched element, like a real DOM would.
const makeTarget = (matches: string | null, contenteditableValue?: string | null) => ({
closest(selectors: string) {
if (matches === null || !selectors.includes(matches)) return null;
return {
getAttribute: (name: string) =>
name === "contenteditable" ? (contenteditableValue ?? null) : null,
};
},
});

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.
expect(surfaceShortcutTargetsTypingContext(makeTarget("[contenteditable]", "true"))).toBe(true);
// An empty attribute value still means editable.
expect(surfaceShortcutTargetsTypingContext(makeTarget("[contenteditable]", ""))).toBe(true);
});

it("ignores non-editable regions and bare focus targets", () => {
expect(surfaceShortcutTargetsTypingContext(null)).toBe(false);
expect(surfaceShortcutTargetsTypingContext(makeTarget(null))).toBe(false);
// Non-editable islands inside an editable host stay shortcut-reachable.
expect(surfaceShortcutTargetsTypingContext(makeTarget("[contenteditable]", "false"))).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. Non-editable islands marked
* `contenteditable="false"` (chips inside the composer, standalone widgets)
* stay shortcut-reachable.
*/
export function surfaceShortcutTargetsTypingContext(
target: {
closest(selectors: string): { getAttribute(qualifiedName: string): string | null } | null;
} | null,
): boolean {
const editable = target?.closest("input, textarea, select, [contenteditable]") ?? null;
return editable != null && editable.getAttribute("contenteditable") !== "false";
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Resolved
}
Comment thread
Lucenx9 marked this conversation as resolved.
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