From 597e59ac04f3f252cf4459a822fe48d778dfc55a Mon Sep 17 00:00:00 2001 From: David Mizrahi Date: Sun, 19 Apr 2026 10:25:59 +0300 Subject: [PATCH] fix(tts): make ElevenLabs voice picker selectable with portaled dropdown (#960) --- ui/web/src/components/voice-picker.tsx | 39 ++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/ui/web/src/components/voice-picker.tsx b/ui/web/src/components/voice-picker.tsx index 4f103c76..211dd279 100644 --- a/ui/web/src/components/voice-picker.tsx +++ b/ui/web/src/components/voice-picker.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from "react"; +import { useEffect, useState, useRef } from "react"; import { useTranslation } from "react-i18next"; import { RefreshCwIcon, ChevronDownIcon } from "lucide-react"; import { createPortal } from "react-dom"; @@ -185,11 +185,38 @@ function DynamicVoicePicker({ refresh(); }; - const handleBlur = (e: React.FocusEvent) => { - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - setOpen(false); + useEffect(() => { + if (!open) { + return; } - }; + + const handlePointerDown = (event: PointerEvent) => { + const target = event.target as Node | null; + if (!target) { + return; + } + + if (triggerRef.current?.contains(target) || dropdownRef.current?.contains(target)) { + return; + } + + setOpen(false); + }; + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setOpen(false); + } + }; + + document.addEventListener("pointerdown", handlePointerDown); + document.addEventListener("keydown", handleEscape); + + return () => { + document.removeEventListener("pointerdown", handlePointerDown); + document.removeEventListener("keydown", handleEscape); + }; + }, [open]); const dropdownContent = open && (
+