diff --git a/ui/web/src/components/voice-picker.tsx b/ui/web/src/components/voice-picker.tsx
index 211dd279..87869abe 100644
--- a/ui/web/src/components/voice-picker.tsx
+++ b/ui/web/src/components/voice-picker.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState, useRef } from "react";
+import { useEffect, useId, useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import { RefreshCwIcon, ChevronDownIcon } from "lucide-react";
import { createPortal } from "react-dom";
@@ -43,6 +43,8 @@ function VoiceRow({ voice, selected, onSelect }: { voice: Voice; selected: boole
return (
(null);
const dropdownRef = useRef
(null);
+ const listboxId = useId();
const { data: voices = [], isLoading } = useVoices();
const { mutate: refresh, isPending: refreshing } = useRefreshVoices();
@@ -168,10 +171,13 @@ function DynamicVoicePicker({
? voices.filter((v) => v.name.toLowerCase().includes(search.toLowerCase()))
: voices;
- const handleOpen = () => {
+ const handleToggle = () => {
if (disabled) return;
- setOpen(true);
- setSearch("");
+ setOpen((prev) => {
+ if (prev) return false;
+ setSearch("");
+ return true;
+ });
};
const handleSelect = (voice: Voice) => {
@@ -209,18 +215,29 @@ function DynamicVoicePicker({
}
};
+ // Close on scroll/resize — position:fixed dropdown does not reflow with
+ // the viewport, so leaving it open in the wrong place is worse than closing.
+ const handleReposition = () => setOpen(false);
+
document.addEventListener("pointerdown", handlePointerDown);
document.addEventListener("keydown", handleEscape);
+ window.addEventListener("scroll", handleReposition, true);
+ window.addEventListener("resize", handleReposition);
return () => {
document.removeEventListener("pointerdown", handlePointerDown);
document.removeEventListener("keydown", handleEscape);
+ window.removeEventListener("scroll", handleReposition, true);
+ window.removeEventListener("resize", handleReposition);
};
}, [open]);
const dropdownContent = open && (
{
if (!triggerRef.current) return {};
@@ -284,7 +301,10 @@ function DynamicVoicePicker({