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 {
6060} from "./composerMentionDrag";
6161import {
6262 composerFloatingLayerProps,
63 isInsideCollapsedComposerControls,
6364 isInsideComposerFloatingLayer,
6465 isInsideRestingComposerControlScope,
6566} from "./composerEventScope";
......@@ -4542,6 +4543,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
45424543 onPointerDownCapture={(event) => {
45434544 const target = event.target;
45444545 if (isInsideRestingComposerControlScope(target)) return;
4546 if (isInsideCollapsedComposerControls(target)) return;
45454547 if (!(target instanceof Element)) return;
45464548 const isInteractive = Boolean(
45474549 target.closest('button, a, input, select, [role="button"], [role="menuitem"]'),
......@@ -4564,11 +4566,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
45644566 if (composerControlsInStrip && isInsideRestingComposerControlScope(activeElement)) {
45654567 return;
45664568 }
4567 if (
4568 isComposerCollapsedMobile &&
4569 activeElement instanceof HTMLElement &&
4570 activeElement.closest('[data-chat-composer-collapsed-controls="true"]')
4571 ) {
4569 if (isInsideCollapsedComposerControls(activeElement)) {
45724570 return;
45734571 }
45744572 // Focus returning from another window or tab lands on the element
apps/web/src/components/chat/composerEventScope.test.ts+12
......@@ -1,6 +1,7 @@
11import { afterEach, describe, expect, it, vi } from "vite-plus/test";
22
33import {
4 isInsideCollapsedComposerControls,
45 isInsideComposerFloatingLayer,
56 isInsideRestingComposerControlScope,
67} from "./composerEventScope";
......@@ -58,4 +59,15 @@ describe("composer event scopes", () => {
5859 expect(isInsideRestingComposerControlScope(target as unknown as EventTarget)).toBe(false);
5960 expect(isInsideRestingComposerControlScope(null)).toBe(false);
6061 });
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 });
6173});
apps/web/src/components/chat/composerEventScope.ts+10
......@@ -11,6 +11,16 @@ export function isInsideComposerFloatingLayer(target: EventTarget | null): boole
1111 return target instanceof Element && target.closest(COMPOSER_FLOATING_LAYER_SELECTOR) !== null;
1212}
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
1424export function isInsideRestingComposerControlScope(target: EventTarget | null): boolean {
1525 return (
1626 target instanceof Element &&