Skip to content

Commit 874e398

Browse files
committed
fix: add language selector dropdown positioning and sizing inside settings modal
1 parent a9dcbbc commit 874e398

File tree

2 files changed

+62
-18
lines changed

2 files changed

+62
-18
lines changed

src/components/SettingsPage.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { getPlatform } from "../utils/platform";
4646
import { ActivationModeSelector } from "./ui/ActivationModeSelector";
4747
import { Toggle } from "./ui/toggle";
4848
import DeveloperSection from "./DeveloperSection";
49+
import LanguageSelector from "./ui/LanguageSelector";
4950
import { Skeleton } from "./ui/skeleton";
5051
import { Progress } from "./ui/progress";
5152
import { useToast } from "./ui/Toast";
@@ -592,6 +593,7 @@ export default function SettingsPage({ activeSection = "general" }: SettingsPage
592593
whisperModel,
593594
localTranscriptionProvider,
594595
parakeetModel,
596+
preferredLanguage,
595597
cloudTranscriptionProvider,
596598
cloudTranscriptionModel,
597599
cloudTranscriptionBaseUrl,
@@ -1358,6 +1360,29 @@ export default function SettingsPage({ activeSection = "general" }: SettingsPage
13581360
</SettingsPanel>
13591361
</div>
13601362

1363+
{/* Language */}
1364+
<div>
1365+
<SectionHeader
1366+
title="Language"
1367+
description="Set the language used for transcription"
1368+
/>
1369+
<SettingsPanel>
1370+
<SettingsPanelRow>
1371+
<SettingsRow
1372+
label="Preferred language"
1373+
description="Choose the language you speak for more accurate transcription"
1374+
>
1375+
<LanguageSelector
1376+
value={preferredLanguage}
1377+
onChange={(value) =>
1378+
updateTranscriptionSettings({ preferredLanguage: value })
1379+
}
1380+
/>
1381+
</SettingsRow>
1382+
</SettingsPanelRow>
1383+
</SettingsPanel>
1384+
</div>
1385+
13611386
{/* Dictation Hotkey */}
13621387
<div>
13631388
<SectionHeader

src/components/ui/LanguageSelector.tsx

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default function LanguageSelector({
2424
const [searchQuery, setSearchQuery] = useState("");
2525
const [highlightedIndex, setHighlightedIndex] = useState(0);
2626
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
27+
const containerRef = useRef<HTMLDivElement>(null);
2728
const dropdownRef = useRef<HTMLDivElement>(null);
2829
const triggerRef = useRef<HTMLButtonElement>(null);
2930
const searchInputRef = useRef<HTMLInputElement>(null);
@@ -38,25 +39,43 @@ export default function LanguageSelector({
3839
setHighlightedIndex(0);
3940
}, [searchQuery]);
4041

42+
// Determine the portal container: use the closest dialog if inside one (to stay
43+
// within Radix's focus trap), otherwise fall back to document.body.
44+
const portalTarget = useRef<HTMLElement>(document.body);
45+
4146
useEffect(() => {
42-
if (isOpen) {
43-
if (searchInputRef.current) {
44-
searchInputRef.current.focus();
45-
}
46-
// Calculate dropdown position
47-
if (triggerRef.current) {
48-
const rect = triggerRef.current.getBoundingClientRect();
49-
setDropdownPosition({
50-
top: rect.bottom + 4, // 4px gap (mt-1)
51-
left: rect.left,
52-
width: rect.width,
53-
});
54-
}
47+
if (containerRef.current) {
48+
const dialog = containerRef.current.closest('[role="dialog"]');
49+
portalTarget.current = (dialog as HTMLElement) ?? document.body;
50+
}
51+
}, []);
52+
53+
useEffect(() => {
54+
if (isOpen && triggerRef.current) {
55+
const triggerRect = triggerRef.current.getBoundingClientRect();
56+
const target = portalTarget.current;
57+
// When portaled into a transformed ancestor (e.g. Radix Dialog),
58+
// fixed positioning is relative to that ancestor, not the viewport.
59+
const offsetX = target === document.body ? 0 : target.getBoundingClientRect().left;
60+
const offsetY = target === document.body ? 0 : target.getBoundingClientRect().top;
61+
setDropdownPosition({
62+
top: triggerRect.bottom + 4 - offsetY,
63+
left: triggerRect.left - offsetX,
64+
width: triggerRect.width,
65+
});
66+
requestAnimationFrame(() => {
67+
searchInputRef.current?.focus();
68+
});
5569
}
5670
}, [isOpen]);
5771
useEffect(() => {
5872
const handleClickOutside = (event: MouseEvent) => {
59-
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
73+
const target = event.target as Node;
74+
if (
75+
containerRef.current &&
76+
!containerRef.current.contains(target) &&
77+
(!dropdownRef.current || !dropdownRef.current.contains(target))
78+
) {
6079
setIsOpen(false);
6180
setSearchQuery("");
6281
}
@@ -111,7 +130,7 @@ export default function LanguageSelector({
111130
};
112131

113132
return (
114-
<div className={`relative ${className}`} ref={dropdownRef}>
133+
<div className={`relative ${className}`} ref={containerRef}>
115134
{/* Trigger button - premium, tight, tactile macOS-style */}
116135
<button
117136
ref={triggerRef}
@@ -120,8 +139,8 @@ export default function LanguageSelector({
120139
onKeyDown={handleKeyDown}
121140
className={`
122141
group relative w-full flex items-center justify-between gap-2
123-
h-9 px-3 text-left
124-
rounded text-sm font-medium
142+
h-7 px-2.5 text-left
143+
rounded text-xs font-medium
125144
border shadow-sm backdrop-blur-sm
126145
transition-all duration-200 ease-out
127146
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:ring-offset-1
@@ -227,7 +246,7 @@ export default function LanguageSelector({
227246
)}
228247
</div>
229248
</div>,
230-
document.body
249+
portalTarget.current
231250
)}
232251
</div>
233252
);

0 commit comments

Comments
 (0)