fix(tui): preserve current selection across list updates (#39774)

This commit is contained in:
Kit Langton 2026-07-30 20:48:51 -04:00 committed by GitHub
commit 865f512a44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 21 additions and 3 deletions

View file

@ -240,7 +240,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
on(
() => props.options,
() => {
if (!props.preserveSelection) {
if (!props.preserveSelection && props.current === undefined) {
const count = flat().length
if (count === 0) return
const next = reconcileSelection(store.selected, count)
@ -276,7 +276,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
setStore("selected", index)
selection = option
if (!moved) return
if (!props.preserveSelection || store.filter.length > 0) return
if ((!props.preserveSelection && props.current === undefined) || store.filter.length > 0) return
scrollAfterLayout(false, option.value)
return
}

View file

@ -79,7 +79,7 @@ async function renderSelect(
return app
}
async function mountSelect(root: string, initial: DialogSelectOption<string>[]) {
async function mountSelect(root: string, initial: DialogSelectOption<string>[], current?: string) {
const state = path.join(root, "state")
await mkdir(state, { recursive: true })
const config = createTuiResolvedConfig()
@ -114,6 +114,7 @@ async function mountSelect(root: string, initial: DialogSelectOption<string>[])
<DialogSelect
title="Mutable options"
options={options()}
current={current}
onMove={(option) => moved.push(option.value)}
onSelect={(option) => selected.push(option.value)}
/>
@ -309,3 +310,20 @@ test("keeps the cursor index while options are temporarily empty", async () => {
select.app.renderer.destroy()
}
})
test("keeps the current option selected when options reorder", async () => {
await using tmp = await tmpdir()
const options = ["first", "current", "third"].map((value) => ({ title: value, value }))
const select = await mountSelect(tmp.path, options, "current")
try {
select.replaceOptions([options[1], options[2], options[0]])
await select.app.waitForFrame((frame) => frame.indexOf("current") < frame.indexOf("third"))
select.app.mockInput.pressEnter()
await select.app.waitFor(() => select.selected.length === 1)
expect(select.selected).toEqual(["current"])
} finally {
select.app.renderer.destroy()
}
})