Fix dropdown clipping: remove overflow constraints on parent containers

Revert fixed-positioning approach (caused z-index and placement issues).
Instead fix the root cause: parent containers with overflow that clipped
absolutely-positioned dropdowns.

- IAMPage: Remove overflow-x-auto on table wrapper (columns already
  hide via responsive classes, no horizontal scroll needed)
- AlertBanner: Remove max-h-48 overflow-y-auto on alerts list
  (alerts are naturally bounded, constraint clipped SnoozeDropdown)
- Revert UserActionsMenu and SnoozeDropdown to simple absolute positioning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-18 01:20:57 +08:00
parent a327890b57
commit 0f6e40a5ba
4 changed files with 8 additions and 35 deletions

View File

@ -160,7 +160,7 @@ export default function IAMPage() {
{searchQuery ? 'No users match your search.' : 'No users found.'} {searchQuery ? 'No users match your search.' : 'No users found.'}
</p> </p>
) : ( ) : (
<div className="overflow-x-auto"> <div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-border bg-card-elevated/50"> <tr className="border-b border-border bg-card-elevated/50">

View File

@ -46,8 +46,6 @@ export default function UserActionsMenu({ user, currentUsername }: UserActionsMe
const [roleSubmenuOpen, setRoleSubmenuOpen] = useState(false); const [roleSubmenuOpen, setRoleSubmenuOpen] = useState(false);
const [tempPassword, setTempPassword] = useState<string | null>(null); const [tempPassword, setTempPassword] = useState<string | null>(null);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const [menuPos, setMenuPos] = useState<{ top: number; right: number } | null>(null);
const updateRole = useUpdateRole(); const updateRole = useUpdateRole();
const resetPassword = useResetPassword(); const resetPassword = useResetPassword();
@ -125,17 +123,10 @@ export default function UserActionsMenu({ user, currentUsername }: UserActionsMe
return ( return (
<div ref={menuRef} className="relative"> <div ref={menuRef} className="relative">
<Button <Button
ref={buttonRef}
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-7 w-7"
onClick={() => { onClick={() => setOpen((v) => !v)}
if (!open && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
setMenuPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
}
setOpen((v) => !v);
}}
aria-label="User actions" aria-label="User actions"
> >
{isLoading ? ( {isLoading ? (
@ -146,10 +137,7 @@ export default function UserActionsMenu({ user, currentUsername }: UserActionsMe
</Button> </Button>
{open && ( {open && (
<div <div className="absolute right-0 top-8 z-50 min-w-[200px] rounded-lg border bg-card shadow-lg py-1">
className="fixed z-50 min-w-[200px] rounded-lg border bg-card shadow-lg py-1"
style={menuPos ? { top: menuPos.top, right: menuPos.right } : undefined}
>
{/* Edit Role */} {/* Edit Role */}
<div className="relative"> <div className="relative">
<button <button

View File

@ -23,7 +23,7 @@ export default function AlertBanner({ alerts, onDismiss, onSnooze }: AlertBanner
{alerts.length} {alerts.length}
</span> </span>
</div> </div>
<div className="divide-y divide-border max-h-48 overflow-y-auto"> <div className="divide-y divide-border">
{alerts.map((alert) => ( {alerts.map((alert) => (
<div <div
key={alert.id} key={alert.id}

View File

@ -17,8 +17,6 @@ const DEFAULT_OPTIONS: { value: number; label: string }[] = [
export default function SnoozeDropdown({ onSnooze, label, direction = 'up', options = DEFAULT_OPTIONS }: SnoozeDropdownProps) { export default function SnoozeDropdown({ onSnooze, label, direction = 'up', options = DEFAULT_OPTIONS }: SnoozeDropdownProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const [menuPos, setMenuPos] = useState<{ top?: number; bottom?: number; right: number } | null>(null);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
@ -41,18 +39,7 @@ export default function SnoozeDropdown({ onSnooze, label, direction = 'up', opti
return ( return (
<div className="relative" ref={ref}> <div className="relative" ref={ref}>
<button <button
ref={buttonRef} onClick={() => setOpen(!open)}
onClick={() => {
if (!open && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
if (direction === 'up') {
setMenuPos({ bottom: window.innerHeight - rect.top + 4, right: window.innerWidth - rect.right });
} else {
setMenuPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
}
}
setOpen(!open);
}}
aria-label={`Snooze "${label}"`} aria-label={`Snooze "${label}"`}
aria-expanded={open} aria-expanded={open}
aria-haspopup="menu" aria-haspopup="menu"
@ -62,11 +49,9 @@ export default function SnoozeDropdown({ onSnooze, label, direction = 'up', opti
<span className="text-[11px] font-medium">Snooze</span> <span className="text-[11px] font-medium">Snooze</span>
</button> </button>
{open && ( {open && (
<div <div role="menu" className={`absolute right-0 w-32 rounded-md border bg-popover shadow-lg z-50 py-1 animate-fade-in ${
role="menu" direction === 'up' ? 'bottom-full mb-1' : 'top-full mt-1'
className="fixed w-32 rounded-md border bg-popover shadow-lg z-50 py-1 animate-fade-in" }`}>
style={menuPos ? { ...menuPos } : undefined}
>
{options.map((opt) => ( {options.map((opt) => (
<button <button
key={opt.value} key={opt.value}