Skip to content

Commit 3586886

Browse files
committed
feat: enhance ApiKeyLink component with dropdown positioning and portal rendering
1 parent 3c799ba commit 3586886

1 file changed

Lines changed: 48 additions & 6 deletions

File tree

src/components/mdx/api-key-link.tsx

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

3-
import { useEffect, useState } from 'react';
3+
import { useEffect, useState, useRef } from 'react';
4+
import { createPortal } from 'react-dom';
45
import { ChevronDown } from 'lucide-react';
56
import { useApiKey } from './api-key-context';
67

@@ -11,6 +12,8 @@ interface ApiKeyLinkProps {
1112
export 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(/\/docs\/?$/, '');
@@ -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

Comments
 (0)