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 { PageLayout } from '@/components/layout/PageLayout';
|
||||
import { PlanBadge } from '@/components/shared/PlanBadge';
|
||||
@@ -27,6 +28,8 @@ export default function Clients() {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = 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 [keyCopied, setKeyCopied] = useState(false);
|
||||
|
||||
@@ -203,35 +206,54 @@ export default function Clients() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setActiveMenu(activeMenu === client.id ? null : client.id)}
|
||||
className="p-1.5 rounded hover:bg-surface-overlay text-content-muted"
|
||||
aria-label="Actions"
|
||||
>
|
||||
<MoreVertical size={16} />
|
||||
</button>
|
||||
{activeMenu === client.id && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10" onClick={() => setActiveMenu(null)} />
|
||||
<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">
|
||||
<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)}>
|
||||
<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)}>
|
||||
<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); }} 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>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
ref={(el) => { if (el) menuButtonRefs.current.set(client.id, el); }}
|
||||
onClick={() => {
|
||||
const isOpen = activeMenu === client.id;
|
||||
if (isOpen) {
|
||||
setActiveMenu(null);
|
||||
setMenuPosition(null);
|
||||
} else {
|
||||
const btn = menuButtonRefs.current.get(client.id);
|
||||
if (btn) {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
setMenuPosition({
|
||||
top: rect.bottom + 4,
|
||||
right: window.innerWidth - rect.right,
|
||||
});
|
||||
}
|
||||
setActiveMenu(client.id);
|
||||
}
|
||||
}}
|
||||
className="p-1.5 rounded hover:bg-surface-overlay text-content-muted"
|
||||
aria-label="Actions"
|
||||
>
|
||||
<MoreVertical size={16} />
|
||||
</button>
|
||||
{activeMenu === client.id && menuPosition && createPortal(
|
||||
<>
|
||||
<div className="fixed inset-0 z-10" onClick={() => { setActiveMenu(null); setMenuPosition(null); }} />
|
||||
<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>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user