@@ -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