Răsfoiți Sursa

fix(tui): ignore stray releases on new session controls (#42673)

Kit Langton 1 zi în urmă
părinte
comite
ab7a0bf65c

+ 12 - 2
packages/tui/src/component/session-tabs.tsx

@@ -345,6 +345,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
   let rail: { screenX: number; screenY: number } | undefined
   let scroll: ScrollBoxRenderable | undefined
   let didDrag = false
+  let addPressed = false
   // A captured drag ends with a synthetic up on its drop target; do not turn that into a click.
   let suppressClick = false
 
@@ -760,7 +761,8 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
               onMouseDown={(event: MouseEvent) => {
                 didDrag = false
                 setDragging(undefined)
-                if (event.button !== RIGHT_MOUSE_BUTTON) return
+                addPressed = event.button !== RIGHT_MOUSE_BUTTON
+                if (addPressed) return
                 if (!rail) return
                 setContextMenu({ x: event.x, y: event.y })
                 event.preventDefault()
@@ -769,8 +771,11 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
               onMouseUp={(event: MouseEvent) => {
                 if (event.button === RIGHT_MOUSE_BUTTON) return
                 if (suppressClick) return
+                if (!addPressed) return
+                addPressed = false
                 if (!newTab()) tabs.add?.()
               }}
+              onMouseDragEnd={() => (addPressed = false)}
             >
               <text
                 width={2}
@@ -837,6 +842,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
   const [contextMenu, setContextMenu] = createSignal<TabContextMenuState>()
   let strip: { screenX: number; screenY: number } | undefined
   let didDrag = false
+  let addPressed = false
   // A captured drag ends with a synthetic up on its drop target; do not turn that into a click.
   let suppressClick = false
   const hueStep = () => (mode() === "light" ? 800 : 200)
@@ -1203,7 +1209,8 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
           onMouseDown={(event) => {
             didDrag = false
             setDragging(undefined)
-            if (event.button !== RIGHT_MOUSE_BUTTON) return
+            addPressed = event.button !== RIGHT_MOUSE_BUTTON
+            if (addPressed) return
             setContextMenu({ x: event.x, y: event.y })
             event.preventDefault()
             event.stopPropagation()
@@ -1211,8 +1218,11 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
           onMouseUp={(event) => {
             if (event.button === RIGHT_MOUSE_BUTTON) return
             if (suppressClick) return
+            if (!addPressed) return
+            addPressed = false
             tabs.add?.()
           }}
+          onMouseDragEnd={() => (addPressed = false)}
         >
           {" + "}
         </text>

+ 62 - 0
packages/tui/test/component/session-tabs-mouse.test.tsx

@@ -0,0 +1,62 @@
+/** @jsxImportSource @opentui/solid */
+import { testRender } from "@opentui/solid"
+import { expect, test } from "bun:test"
+import { createSignal } from "solid-js"
+import { ConfigProvider } from "../../src/config"
+import { EMPTY_SESSION_TAB_STATUS, SessionTabs, type SessionTabsController } from "../../src/component/session-tabs"
+import { ThemeProvider } from "../../src/context/theme"
+import { emptyThemeSource } from "../fixture/fixture"
+import { TestTuiContexts } from "../fixture/tui-environment"
+import { createTuiResolvedConfig } from "../fixture/tui-runtime"
+
+test("releasing a transcript selection over tab controls does not activate them", async () => {
+  const [active, setActive] = createSignal("first")
+  const [added, setAdded] = createSignal(0)
+  const controller = {
+    tabs: () => [
+      { sessionID: "first", title: "First" },
+      { sessionID: "second", title: "Second" },
+    ],
+    current: active,
+    select: setActive,
+    close() {},
+    move() {},
+    add: () => setAdded((value) => value + 1),
+    status: () => EMPTY_SESSION_TAB_STATUS,
+  } satisfies SessionTabsController
+  const app = await testRender(
+    () => (
+      <TestTuiContexts>
+        <ConfigProvider config={createTuiResolvedConfig({ tabs: { enabled: true } })}>
+          <ThemeProvider mode="dark" source={emptyThemeSource}>
+            <box flexDirection="column">
+              <SessionTabs controller={controller} animations={false} />
+              <text>selectable transcript text</text>
+            </box>
+          </ThemeProvider>
+        </ConfigProvider>
+      </TestTuiContexts>
+    ),
+    { width: 60, height: 3 },
+  )
+
+  try {
+    app.renderer.start()
+    await app.waitForFrame((frame) => frame.includes("Second"))
+    await app.mockMouse.pressDown(5, 1)
+    await app.mockMouse.release(40, 0)
+    expect(active()).toBe("first")
+
+    await app.mockMouse.click(40, 0)
+    expect(active()).toBe("second")
+
+    await app.mockMouse.pressDown(5, 1)
+    await app.mockMouse.release(58, 0)
+    expect(added()).toBe(0)
+
+    await app.mockMouse.click(58, 0)
+    expect(added()).toBe(1)
+  } finally {
+    app.renderer.destroy()
+  }
+})