Fix dropdown menu: use React portal to render outside table overflow
CI / Lint & Format (push) Failing after 8s
CI / Tests (push) Has been skipped
CI / Security Scan (push) Failing after 7s
CI / Docker Build (push) Has been skipped

- 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:
2026-06-25 22:13:11 -04:00
parent 009e42b3e2
commit 317c73407e
+52 -30
View File
@@ -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>
))}