authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-03 11:09:51-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 11:09:51-07:00
log126afb56fca56c663f4f40acd46f123ab27eee2b
treee02ef39798fc475504c301f5a340394658f51a20
parent8bd544cdfd22aa38ab82bc1adc0b851755a299ef
signature Signed by PGP key B5690EEEBB952194

fix(web): banner buttons no longer expand the resting composer (#9452)


3 files changed, 25 insertions(+), 5 deletions(-)

apps/web/src/components/chat/ChatComposer.tsx+3-5
...@@ -60,6 +60,7 @@ import {...@@ -60,6 +60,7 @@ import {
60} from "./composerMentionDrag";60} from "./composerMentionDrag";
61import {61import {
62 composerFloatingLayerProps,62 composerFloatingLayerProps,
63 isInsideCollapsedComposerControls,
63 isInsideComposerFloatingLayer,64 isInsideComposerFloatingLayer,
64 isInsideRestingComposerControlScope,65 isInsideRestingComposerControlScope,
65} from "./composerEventScope";66} from "./composerEventScope";
...@@ -4542,6 +4543,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -4542,6 +4543,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
4542 onPointerDownCapture={(event) => {4543 onPointerDownCapture={(event) => {
4543 const target = event.target;4544 const target = event.target;
4544 if (isInsideRestingComposerControlScope(target)) return;4545 if (isInsideRestingComposerControlScope(target)) return;
4546 if (isInsideCollapsedComposerControls(target)) return;
4545 if (!(target instanceof Element)) return;4547 if (!(target instanceof Element)) return;
4546 const isInteractive = Boolean(4548 const isInteractive = Boolean(
4547 target.closest('button, a, input, select, [role="button"], [role="menuitem"]'),4549 target.closest('button, a, input, select, [role="button"], [role="menuitem"]'),
...@@ -4564,11 +4566,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -4564,11 +4566,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
4564 if (composerControlsInStrip && isInsideRestingComposerControlScope(activeElement)) {4566 if (composerControlsInStrip && isInsideRestingComposerControlScope(activeElement)) {
4565 return;4567 return;
4566 }4568 }
4567 if (4569 if (isInsideCollapsedComposerControls(activeElement)) {
4568 isComposerCollapsedMobile &&
4569 activeElement instanceof HTMLElement &&
4570 activeElement.closest('[data-chat-composer-collapsed-controls="true"]')
4571 ) {
4572 return;4570 return;
4573 }4571 }
4574 // Focus returning from another window or tab lands on the element4572 // Focus returning from another window or tab lands on the element
apps/web/src/components/chat/composerEventScope.test.ts+12
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import { afterEach, describe, expect, it, vi } from "vite-plus/test";1import { afterEach, describe, expect, it, vi } from "vite-plus/test";
22
3import {3import {
4 isInsideCollapsedComposerControls,
4 isInsideComposerFloatingLayer,5 isInsideComposerFloatingLayer,
5 isInsideRestingComposerControlScope,6 isInsideRestingComposerControlScope,
6} from "./composerEventScope";7} from "./composerEventScope";
...@@ -58,4 +59,15 @@ describe("composer event scopes", () => {...@@ -58,4 +59,15 @@ describe("composer event scopes", () => {
58 expect(isInsideRestingComposerControlScope(target as unknown as EventTarget)).toBe(false);59 expect(isInsideRestingComposerControlScope(target as unknown as EventTarget)).toBe(false);
59 expect(isInsideRestingComposerControlScope(null)).toBe(false);60 expect(isInsideRestingComposerControlScope(null)).toBe(false);
60 });61 });
62
63 it("recognizes banner and drawer controls docked above the surface", () => {
64 vi.stubGlobal("Element", FakeElement);
65
66 const target = new FakeElement('[data-chat-composer-collapsed-controls="true"]');
67 expect(isInsideCollapsedComposerControls(target as unknown as EventTarget)).toBe(true);
68 expect(isInsideCollapsedComposerControls(new FakeElement(null) as unknown as EventTarget)).toBe(
69 false,
70 );
71 expect(isInsideCollapsedComposerControls(null)).toBe(false);
72 });
61});73});
apps/web/src/components/chat/composerEventScope.ts+10
...@@ -11,6 +11,16 @@ export function isInsideComposerFloatingLayer(target: EventTarget | null): boole...@@ -11,6 +11,16 @@ export function isInsideComposerFloatingLayer(target: EventTarget | null): boole
11 return target instanceof Element && target.closest(COMPOSER_FLOATING_LAYER_SELECTOR) !== null;11 return target instanceof Element && target.closest(COMPOSER_FLOATING_LAYER_SELECTOR) !== null;
12}12}
1313
14// Banners, the approval row, and the tasks badge dock above the surface. A
15// pointer or focus landing on one of them acts on that control and must not
16// expand a resting or collapsed composer.
17export function isInsideCollapsedComposerControls(target: EventTarget | null): boolean {
18 return (
19 target instanceof Element &&
20 target.closest('[data-chat-composer-collapsed-controls="true"]') !== null
21 );
22}
23
14export function isInsideRestingComposerControlScope(target: EventTarget | null): boolean {24export function isInsideRestingComposerControlScope(target: EventTarget | null): boolean {
15 return (25 return (
16 target instanceof Element &&26 target instanceof Element &&