| 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,6 +60,7 @@ import { |
| 60 | } from "./composerMentionDrag"; | 60 | } from "./composerMentionDrag"; |
| 61 | import { | 61 | import { |
| 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 element | 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,6 +1,7 @@ |
| 1 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; | 1 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 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 | } |
| 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 | export function isInsideRestingComposerControlScope(target: EventTarget | null): boolean { | 24 | export function isInsideRestingComposerControlScope(target: EventTarget | null): boolean { |
| 15 | return ( | 25 | return ( |
| 16 | target instanceof Element && | 26 | target instanceof Element && |