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({