feat(chat): add ESC key support and improve positioning for UserProfilePopover

This commit is contained in:
Chneemann 2026-09-07 18:20:27 +02:00
parent 98a982b9fb
commit ac964ea98b
No known key found for this signature in database
2 changed files with 19 additions and 3 deletions

View file

@ -83,6 +83,7 @@ export function ChatItem({
? `/api/dm/messages/${message.id}` ? `/api/dm/messages/${message.id}`
: `/api/messages/${message.id}`; : `/api/messages/${message.id}`;
/** Closes the profile popover when clicking outside or pressing the Escape key. */
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if ( if (
@ -93,14 +94,24 @@ export function ChatItem({
} }
}; };
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setIsProfileOpen(false);
}
};
if (isProfileOpen) { if (isProfileOpen) {
document.addEventListener("mousedown", handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleKeyDown);
} }
return () => { return () => {
document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleKeyDown);
}; };
}, [isProfileOpen]); }, [isProfileOpen]);
/** Determines the friendship status between the current user and the message author. */
const getFriendshipStatus = () => { const getFriendshipStatus = () => {
if ( if (
!user?.id || !user?.id ||

View file

@ -40,25 +40,30 @@ export function UserProfilePopover({
const [justSent, setJustSent] = useState(false); const [justSent, setJustSent] = useState(false);
const isSelf = user.id === currentUserId; const isSelf = user.id === currentUserId;
const isAlreadyFriend = friendshipStatus === "ACCEPTED"; const isAlreadyFriend = friendshipStatus === "ACCEPTED";
const isPending = friendshipStatus === "PENDING" || justSent; const isPending = friendshipStatus === "PENDING" || justSent;
const isButtonDisabled = isLoading || isAlreadyFriend || isPending; const isButtonDisabled = isLoading || isAlreadyFriend || isPending;
/** Precise positioning with Y- and X-axis corrections. */
useLayoutEffect(() => { useLayoutEffect(() => {
if (!triggerRef.current || !popoverRef.current) return; if (!triggerRef.current || !popoverRef.current) return;
const triggerRect = triggerRef.current.getBoundingClientRect(); const triggerRect = triggerRef.current.getBoundingClientRect();
const popoverRect = popoverRef.current.getBoundingClientRect(); const popoverRect = popoverRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight; const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
let top = triggerRect.bottom + 8; let top = triggerRect.bottom + 8;
const left = triggerRect.left; let left = triggerRect.left;
if (top + popoverRect.height > viewportHeight - 16) { if (top + popoverRect.height > viewportHeight - 16) {
top = triggerRect.top - popoverRect.height - 8; top = triggerRect.top - popoverRect.height - 8;
} }
if (left + popoverRect.width > viewportWidth - 16) {
left = viewportWidth - popoverRect.width - 16;
}
setCoords({ top, left }); setCoords({ top, left });
setIsVisible(true); setIsVisible(true);
}, [triggerRef]); }, [triggerRef]);
@ -89,7 +94,7 @@ export function UserProfilePopover({
}} }}
className="z-50 w-64 bg-background border border-surface/80 rounded-2xl p-4 shadow-2xl transition-opacity duration-75 pointer-events-auto" className="z-50 w-64 bg-background border border-surface/80 rounded-2xl p-4 shadow-2xl transition-opacity duration-75 pointer-events-auto"
> >
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3">
<UserAvatar user={user} size="sm" /> <UserAvatar user={user} size="sm" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-foreground truncate leading-tight"> <p className="text-sm font-semibold text-foreground truncate leading-tight">