11'use client' ;
22
3- import { useEffect , useState } from 'react' ;
3+ import { useEffect , useState , useRef } from 'react' ;
4+ import { createPortal } from 'react-dom' ;
45import { ChevronDown } from 'lucide-react' ;
56import { useApiKey } from './api-key-context' ;
67
@@ -11,6 +12,8 @@ interface ApiKeyLinkProps {
1112export function ApiKeyLink ( { text } : ApiKeyLinkProps ) {
1213 const [ isZh , setIsZh ] = useState ( false ) ;
1314 const [ isOpen , setIsOpen ] = useState ( false ) ;
15+ const [ dropdownPosition , setDropdownPosition ] = useState ( { top : 0 , left : 0 } ) ;
16+ const buttonRef = useRef < HTMLButtonElement > ( null ) ;
1417 const { apiKeys, selectedApiKey, setSelectedApiKey, isLoading, error } = useApiKey ( ) ;
1518
1619 useEffect ( ( ) => {
@@ -19,6 +22,37 @@ export function ApiKeyLink({ text }: ApiKeyLinkProps) {
1922 }
2023 } , [ ] ) ;
2124
25+ // Update dropdown position
26+ const updatePosition = ( ) => {
27+ if ( buttonRef . current ) {
28+ const rect = buttonRef . current . getBoundingClientRect ( ) ;
29+ // getBoundingClientRect returns viewport-relative position
30+ // position: fixed also uses viewport-relative position
31+ setDropdownPosition ( {
32+ top : rect . bottom + 4 ,
33+ left : rect . left ,
34+ } ) ;
35+ }
36+ } ;
37+
38+ // Update position when opening and on scroll/resize
39+ useEffect ( ( ) => {
40+ if ( isOpen ) {
41+ updatePosition ( ) ;
42+
43+ // Listen for scroll and resize events to update position
44+ const handleScrollOrResize = ( ) => updatePosition ( ) ;
45+
46+ window . addEventListener ( 'scroll' , handleScrollOrResize , true ) ;
47+ window . addEventListener ( 'resize' , handleScrollOrResize ) ;
48+
49+ return ( ) => {
50+ window . removeEventListener ( 'scroll' , handleScrollOrResize , true ) ;
51+ window . removeEventListener ( 'resize' , handleScrollOrResize ) ;
52+ } ;
53+ }
54+ } , [ isOpen ] ) ;
55+
2256 const handleClick = ( ) => {
2357 if ( typeof window !== 'undefined' ) {
2458 const origin = window . location . origin . replace ( / \/ d o c s \/ ? $ / , '' ) ;
@@ -52,8 +86,9 @@ export function ApiKeyLink({ text }: ApiKeyLinkProps) {
5286 : '' ;
5387
5488 return (
55- < span className = "relative inline-block" >
89+ < span className = "inline-block" >
5690 < button
91+ ref = { buttonRef }
5792 onClick = { ( ) => setIsOpen ( ! isOpen ) }
5893 className = "fd-inline-code cursor-pointer inline-flex items-center gap-1 hover:text-fd-primary transition-colors"
5994 title = { isZh ? '点击选择 API Key' : 'Click to select API Key' }
@@ -62,15 +97,21 @@ export function ApiKeyLink({ text }: ApiKeyLinkProps) {
6297 < ChevronDown className = "size-3" />
6398 </ button >
6499
65- { isOpen && (
100+ { isOpen && typeof document !== 'undefined' && createPortal (
66101 < >
67102 { /* Backdrop to close dropdown */ }
68103 < div
69- className = "fixed inset-0 z-40 "
104+ className = "fixed inset-0 z-[9998] "
70105 onClick = { ( ) => setIsOpen ( false ) }
71106 />
72107 { /* Dropdown menu */ }
73- < div className = "absolute left-0 top-full mt-1 z-50 min-w-[200px] rounded-md border bg-fd-popover p-1 shadow-md" >
108+ < div
109+ className = "fixed z-[9999] min-w-[200px] rounded-md border bg-fd-popover p-1 shadow-lg"
110+ style = { {
111+ top : dropdownPosition . top ,
112+ left : dropdownPosition . left ,
113+ } }
114+ >
74115 { apiKeys . map ( ( apiKey ) => (
75116 < button
76117 key = { apiKey . key }
@@ -97,7 +138,8 @@ export function ApiKeyLink({ text }: ApiKeyLinkProps) {
97138 </ button >
98139 </ div >
99140 </ div >
100- </ >
141+ </ > ,
142+ document . body
101143 ) }
102144 </ span >
103145 ) ;
0 commit comments