Fix dropdown menu: use React portal to render outside table overflow
- createPortal renders menu at document.body level, bypassing all parent overflow/scroll containers - Fixed positioning calculated from button's getBoundingClientRect() - menuButtonRefs map ensures correct position per row
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { PageLayout } from '@/components/layout/PageLayout';
|
import { PageLayout } from '@/components/layout/PageLayout';
|
||||||
import { PlanBadge } from '@/components/shared/PlanBadge';
|
import { PlanBadge } from '@/components/shared/PlanBadge';
|
||||||
@@ -27,6 +28,8 @@ export default function Clients() {
|
|||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
||||||
const [activeMenu, setActiveMenu] = useState<string | null>(null);
|
const [activeMenu, setActiveMenu] = useState<string | null>(null);
|
||||||
|
const [menuPosition, setMenuPosition] = useState<{ top: number; right: number } | null>(null);
|
||||||
|
const menuButtonRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
|
||||||
const [newApiKey, setNewApiKey] = useState<string | null>(null);
|
const [newApiKey, setNewApiKey] = useState<string | null>(null);
|
||||||
const [keyCopied, setKeyCopied] = useState(false);
|
const [keyCopied, setKeyCopied] = useState(false);
|
||||||
|
|
||||||
@@ -203,35 +206,54 @@ export default function Clients() {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-4">
|
<td className="py-3 px-4">
|
||||||
<div className="relative">
|
<button
|
||||||
<button
|
ref={(el) => { if (el) menuButtonRefs.current.set(client.id, el); }}
|
||||||
onClick={() => setActiveMenu(activeMenu === client.id ? null : client.id)}
|
onClick={() => {
|
||||||
className="p-1.5 rounded hover:bg-surface-overlay text-content-muted"
|
const isOpen = activeMenu === client.id;
|
||||||
aria-label="Actions"
|
if (isOpen) {
|
||||||
>
|
setActiveMenu(null);
|
||||||
<MoreVertical size={16} />
|
setMenuPosition(null);
|
||||||
</button>
|
} else {
|
||||||
{activeMenu === client.id && (
|
const btn = menuButtonRefs.current.get(client.id);
|
||||||
<>
|
if (btn) {
|
||||||
<div className="fixed inset-0 z-10" onClick={() => setActiveMenu(null)} />
|
const rect = btn.getBoundingClientRect();
|
||||||
<div className="absolute right-0 top-8 z-20 bg-surface-raised border border-border rounded-lg shadow-xl py-1 w-48 animate-slide-in">
|
setMenuPosition({
|
||||||
<Link to={`/admin/clients/${client.id}`} className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-surface-overlay text-content-secondary" onClick={() => setActiveMenu(null)}>
|
top: rect.bottom + 4,
|
||||||
<Eye size={14} /> Voir détail
|
right: window.innerWidth - rect.right,
|
||||||
</Link>
|
});
|
||||||
<Link to={`/gallery/client/${client.id}`} className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-surface-overlay text-content-secondary" onClick={() => setActiveMenu(null)}>
|
}
|
||||||
<Images size={14} /> Voir galerie
|
setActiveMenu(client.id);
|
||||||
</Link>
|
}
|
||||||
<button onClick={() => { handleRotateKey(client.id); }} className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-surface-overlay text-content-secondary w-full text-left">
|
}}
|
||||||
<KeyRound size={14} /> Régénérer clé
|
className="p-1.5 rounded hover:bg-surface-overlay text-content-muted"
|
||||||
</button>
|
aria-label="Actions"
|
||||||
<hr className="my-1 border-border" />
|
>
|
||||||
<button onClick={() => { setConfirmDelete(client.id); setActiveMenu(null); }} className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-status-error/10 text-status-error w-full text-left">
|
<MoreVertical size={16} />
|
||||||
<UserMinus size={14} /> Désactiver
|
</button>
|
||||||
</button>
|
{activeMenu === client.id && menuPosition && createPortal(
|
||||||
</div>
|
<>
|
||||||
</>
|
<div className="fixed inset-0 z-10" onClick={() => { setActiveMenu(null); setMenuPosition(null); }} />
|
||||||
)}
|
<div
|
||||||
</div>
|
className="fixed z-20 bg-surface-raised border border-border rounded-lg shadow-xl py-1 w-48 animate-slide-in"
|
||||||
|
style={{ top: menuPosition.top, right: menuPosition.right }}
|
||||||
|
>
|
||||||
|
<Link to={`/admin/clients/${client.id}`} className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-surface-overlay text-content-secondary" onClick={() => { setActiveMenu(null); setMenuPosition(null); }}>
|
||||||
|
<Eye size={14} /> Voir détail
|
||||||
|
</Link>
|
||||||
|
<Link to={`/gallery/client/${client.id}`} className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-surface-overlay text-content-secondary" onClick={() => { setActiveMenu(null); setMenuPosition(null); }}>
|
||||||
|
<Images size={14} /> Voir galerie
|
||||||
|
</Link>
|
||||||
|
<button onClick={() => { handleRotateKey(client.id); }} className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-surface-overlay text-content-secondary w-full text-left">
|
||||||
|
<KeyRound size={14} /> Régénérer clé
|
||||||
|
</button>
|
||||||
|
<hr className="my-1 border-border" />
|
||||||
|
<button onClick={() => { setConfirmDelete(client.id); setActiveMenu(null); setMenuPosition(null); }} className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-status-error/10 text-status-error w-full text-left">
|
||||||
|
<UserMinus size={14} /> Désactiver
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user