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
8 changes: 3 additions & 5 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,6 +60,7 @@ import {
} from "./composerMentionDrag";
import {
composerFloatingLayerProps,
isInsideCollapsedComposerControls,
isInsideComposerFloatingLayer,
isInsideRestingComposerControlScope,
} from "./composerEventScope";
Expand DownExpand Up@@ -4542,6 +4543,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
onPointerDownCapture={(event) => {
const target = event.target;
if (isInsideRestingComposerControlScope(target)) return;
if (isInsideCollapsedComposerControls(target)) return;
if (!(target instanceof Element)) return;
const isInteractive = Boolean(
target.closest('button, a, input, select, [role="button"], [role="menuitem"]'),
Expand All@@ -4564,11 +4566,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
if (composerControlsInStrip && isInsideRestingComposerControlScope(activeElement)) {
return;
}
if (
isComposerCollapsedMobile &&
activeElement instanceof HTMLElement &&
activeElement.closest('[data-chat-composer-collapsed-controls="true"]')
) {
if (isInsideCollapsedComposerControls(activeElement)) {
return;
}
// Focus returning from another window or tab lands on the element
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/components/chat/composerEventScope.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vite-plus/test";

import {
isInsideCollapsedComposerControls,
isInsideComposerFloatingLayer,
isInsideRestingComposerControlScope,
} from "./composerEventScope";
Expand DownExpand Up@@ -58,4 +59,15 @@ describe("composer event scopes", () => {
expect(isInsideRestingComposerControlScope(target as unknown as EventTarget)).toBe(false);
expect(isInsideRestingComposerControlScope(null)).toBe(false);
});

it("recognizes banner and drawer controls docked above the surface", () => {
vi.stubGlobal("Element", FakeElement);

const target = new FakeElement('[data-chat-composer-collapsed-controls="true"]');
expect(isInsideCollapsedComposerControls(target as unknown as EventTarget)).toBe(true);
expect(isInsideCollapsedComposerControls(new FakeElement(null) as unknown as EventTarget)).toBe(
false,
);
expect(isInsideCollapsedComposerControls(null)).toBe(false);
});
});
10 changes: 10 additions & 0 deletions apps/web/src/components/chat/composerEventScope.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,16 @@ export function isInsideComposerFloatingLayer(target: EventTarget | null): boole
return target instanceof Element && target.closest(COMPOSER_FLOATING_LAYER_SELECTOR) !== null;
}

// Banners, the approval row, and the tasks badge dock above the surface. A
// pointer or focus landing on one of them acts on that control and must not
// expand a resting or collapsed composer.
export function isInsideCollapsedComposerControls(target: EventTarget | null): boolean {
return (
target instanceof Element &&
target.closest('[data-chat-composer-collapsed-controls="true"]') !== null
);
}

export function isInsideRestingComposerControlScope(target: EventTarget | null): boolean {
return (
target instanceof Element &&
Expand Down
Loading