Fix dropdown clipping: use fixed positioning to escape overflow
UserActionsMenu and SnoozeDropdown were clipped by parent containers with overflow-x-auto/overflow-y-auto. Switch from absolute to fixed positioning — compute viewport-relative coordinates on open via getBoundingClientRect. Dropdowns now render above all overflow boundaries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
44e6c8e3e5
commit
a327890b57
@ -46,6 +46,8 @@ export default function UserActionsMenu({ user, currentUsername }: UserActionsMe
|
||||
const [roleSubmenuOpen, setRoleSubmenuOpen] = useState(false);
|
||||
const [tempPassword, setTempPassword] = useState<string | null>(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 resetPassword = useResetPassword();
|
||||
@ -123,10 +125,17 @@ export default function UserActionsMenu({ user, currentUsername }: UserActionsMe
|
||||
return (
|
||||
<div ref={menuRef} className="relative">
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
onClick={() => {
|
||||
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"
|
||||
>
|
||||
{isLoading ? (
|
||||
@ -137,7 +146,10 @@ export default function UserActionsMenu({ user, currentUsername }: UserActionsMe
|
||||
</Button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute right-0 top-8 z-50 min-w-[200px] rounded-lg border bg-card shadow-lg py-1">
|
||||
<div
|
||||
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 */}
|
||||
<div className="relative">
|
||||
<button
|
||||
|
||||
@ -17,6 +17,8 @@ const DEFAULT_OPTIONS: { value: number; label: string }[] = [
|
||||
export default function SnoozeDropdown({ onSnooze, label, direction = 'up', options = DEFAULT_OPTIONS }: SnoozeDropdownProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const [menuPos, setMenuPos] = useState<{ top?: number; bottom?: number; right: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@ -39,7 +41,18 @@ export default function SnoozeDropdown({ onSnooze, label, direction = 'up', opti
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
ref={buttonRef}
|
||||
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-expanded={open}
|
||||
aria-haspopup="menu"
|
||||
@ -49,9 +62,11 @@ export default function SnoozeDropdown({ onSnooze, label, direction = 'up', opti
|
||||
<span className="text-[11px] font-medium">Snooze</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div role="menu" className={`absolute right-0 w-32 rounded-md border bg-popover shadow-lg z-50 py-1 animate-fade-in ${
|
||||
direction === 'up' ? 'bottom-full mb-1' : 'top-full mt-1'
|
||||
}`}>
|
||||
<div
|
||||
role="menu"
|
||||
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) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user