| author | |
| committer | |
| log | 126afb56fca56c663f4f40acd46f123ab27eee2b |
| tree | e02ef39798fc475504c301f5a340394658f51a20 |
| parent | 8bd544cdfd22aa38ab82bc1adc0b851755a299ef |
| signature | Signed by PGP key B5690EEEBB952194 |
3 files changed, 25 insertions(+), 5 deletions(-)
apps/web/src/components/chat/ChatComposer.tsx+3-5| ... | ... | @@ -60,6 +60,7 @@ import { |
| 60 | 60 | } from "./composerMentionDrag"; |
| 61 | 61 | import { |
| 62 | 62 | composerFloatingLayerProps, |
| 63 | isInsideCollapsedComposerControls, | |
| 63 | 64 | isInsideComposerFloatingLayer, |
| 64 | 65 | isInsideRestingComposerControlScope, |
| 65 | 66 | } from "./composerEventScope"; |
| ... | ... | @@ -4542,6 +4543,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 4542 | 4543 | onPointerDownCapture={(event) => { |
| 4543 | 4544 | const target = event.target; |
| 4544 | 4545 | if (isInsideRestingComposerControlScope(target)) return; |
| 4546 | if (isInsideCollapsedComposerControls(target)) return; | |
| 4545 | 4547 | if (!(target instanceof Element)) return; |
| 4546 | 4548 | const isInteractive = Boolean( |
| 4547 | 4549 | target.closest('button, a, input, select, [role="button"], [role="menuitem"]'), |
| ... | ... | @@ -4564,11 +4566,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 4564 | 4566 | if (composerControlsInStrip && isInsideRestingComposerControlScope(activeElement)) { |
| 4565 | 4567 | return; |
| 4566 | 4568 | } |
| 4567 | if ( | |
| 4568 | isComposerCollapsedMobile && | |
| 4569 | activeElement instanceof HTMLElement && | |
| 4570 | activeElement.closest('[data-chat-composer-collapsed-controls="true"]') | |
| 4571 | ) { | |
| 4569 | if (isInsideCollapsedComposerControls(activeElement)) { | |
| 4572 | 4570 | return; |
| 4573 | 4571 | } |
| 4574 | 4572 | // 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 | 1 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; |
| 2 | 2 | |
| 3 | 3 | import { |
| 4 | isInsideCollapsedComposerControls, | |
| 4 | 5 | isInsideComposerFloatingLayer, |
| 5 | 6 | isInsideRestingComposerControlScope, |
| 6 | 7 | } from "./composerEventScope"; |
| ... | ... | @@ -58,4 +59,15 @@ describe("composer event scopes", () => { |
| 58 | 59 | expect(isInsideRestingComposerControlScope(target as unknown as EventTarget)).toBe(false); |
| 59 | 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 | 11 | return target instanceof Element && target.closest(COMPOSER_FLOATING_LAYER_SELECTOR) !== null; |
| 12 | 12 | } |
| 13 | 13 | |
| 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. | |
| 17 | export 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 | ||
| 14 | 24 | export function isInsideRestingComposerControlScope(target: EventTarget | null): boolean { |
| 15 | 25 | return ( |
| 16 | 26 | target instanceof Element && |