Ver Fonte

fix(tui): focus palette settings after layout (#39585)

Kit Langton há 2 semanas atrás
pai
commit
8e3e94aa26

+ 32 - 24
packages/tui/src/ui/dialog-select.tsx

@@ -1,10 +1,10 @@
-import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
+import { CliRenderEvents, InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
 import { Keymap, type KeymapCommand } from "../context/keymap"
 import { useTheme, useThemes } from "../context/theme"
 import { entries, filter, flatMap, groupBy, pipe } from "remeda"
 import { batch, createEffect, createMemo, createSignal, For, Show, type JSX, on, onCleanup } from "solid-js"
 import { createStore } from "solid-js/store"
-import { useTerminalDimensions } from "@opentui/solid"
+import { useRenderer, useTerminalDimensions } from "@opentui/solid"
 import * as fuzzysort from "fuzzysort"
 import { isDeepEqual } from "remeda"
 import { useDialog, type DialogContext } from "./dialog"
@@ -100,6 +100,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
   const mode = themes.mode
   const config = useConfig().data
   const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
+  const renderer = useRenderer()
 
   const [store, setStore] = createStore({
     selected: 0,
@@ -110,7 +111,18 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
   const actionFocused = createMemo(() => focusedAction() !== undefined)
   let selection: { value: T; category?: string } | undefined
   let resetSelection = false
-  let visibilityGeneration = 0
+  let pendingScroll: (() => void) | undefined
+
+  function scrollAfterLayout(center: boolean, value: T) {
+    if (pendingScroll) renderer.off(CliRenderEvents.FRAME, pendingScroll)
+    pendingScroll = () => {
+      pendingScroll = undefined
+      if (!isDeepEqual(selected()?.value, value)) return
+      scrollToSelection(center)
+    }
+    renderer.once(CliRenderEvents.FRAME, pendingScroll)
+    renderer.requestRender()
+  }
 
   createEffect(
     on(
@@ -264,16 +276,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
           setStore("selected", index)
           selection = option
           if (!moved) return
-          const value = option.value
-          const generation = ++visibilityGeneration
-          requestAnimationFrame(() => {
-            requestAnimationFrame(() => {
-              if (generation !== visibilityGeneration) return
-              if (!props.preserveSelection || store.filter.length > 0) return
-              if (!isDeepEqual(selected()?.value, value)) return
-              scrollToSelection(false)
-            })
-          })
+          if (!props.preserveSelection || store.filter.length > 0) return
+          scrollAfterLayout(false, option.value)
           return
         }
         const next = reconcileSelection(store.selected, flat().length)
@@ -284,22 +288,26 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
     ),
   )
   onCleanup(() => {
-    visibilityGeneration++
+    if (!pendingScroll) return
+    renderer.off(CliRenderEvents.FRAME, pendingScroll)
+    pendingScroll = undefined
   })
 
   createEffect(
     on([() => store.filter, () => props.current], ([filter, current]) => {
       if (filter.length > 0) resetSelection = true
-      setTimeout(() => {
-        if (filter.length > 0) {
-          moveTo(0, true, false)
-        } else if (current && props.focusCurrent !== false) {
-          const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current))
-          if (currentIndex >= 0) {
-            moveTo(currentIndex, true)
-          }
-        }
-      }, 0)
+      if (filter.length > 0) {
+        const option = flat()[0]
+        if (!option) return
+        moveTo(0, true, false)
+        scrollAfterLayout(true, option.value)
+        return
+      }
+      if (!current || props.focusCurrent === false) return
+      const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current))
+      if (currentIndex < 0) return
+      moveTo(currentIndex, true)
+      scrollAfterLayout(true, current)
     }),
   )
 

+ 4 - 8
packages/tui/test/cli/tui/command-palette.test.tsx

@@ -73,16 +73,12 @@ test("searches settings globally and opens the matching setting", async () => {
     expect(app.captureCharFrame()).not.toContain("Animations")
     await app.waitFor(() => app.renderer.currentFocusedEditor instanceof InputRenderable)
 
-    for (const key of "side") app.mockInput.pressKey(key)
-    await app.waitForFrame((frame) => frame.includes("Sidebar"))
-    expect(app.captureCharFrame()).not.toContain("New session")
-    expect(app.captureCharFrame()).not.toContain("Switch model")
-    expect(app.captureCharFrame()).not.toContain("Markdown")
-
+    app.mockInput.pressArrow("down")
+    for (const key of "sounds") app.mockInput.pressKey(key)
     app.mockInput.pressEnter()
-    await app.waitForFrame((frame) => frame.includes("Settings") && frame.includes("Color mode"))
+    await app.waitForFrame((frame) => frame.includes("Settings") && frame.includes("Sounds"))
     app.mockInput.pressEnter()
-    await app.waitFor(() => current.session?.sidebar === "hide")
+    await app.waitFor(() => current.attention?.sound === false)
   } finally {
     app.renderer.destroy()
   }