mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-27 19:25:15 +02:00
refactor: replace ContextMenu with DropdownMenu in SearchSpaceAvatar for improved mobile and desktop interactions
This commit is contained in:
parent
f5f2456dfd
commit
8c91c9d76b
1 changed files with 52 additions and 66 deletions
|
|
@ -2,14 +2,9 @@
|
||||||
|
|
||||||
import { Settings, Trash2, Users } from "lucide-react";
|
import { Settings, Trash2, Users } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import type { MouseEvent } from "react";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
|
||||||
ContextMenu,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuTrigger,
|
|
||||||
} from "@/components/ui/context-menu";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -80,19 +75,28 @@ export function SearchSpaceAvatar({
|
||||||
const initials = getInitials(name);
|
const initials = getInitials(name);
|
||||||
const sizeClasses = size === "sm" ? "h-8 w-8 text-xs" : "h-10 w-10 text-sm";
|
const sizeClasses = size === "sm" ? "h-8 w-8 text-xs" : "h-10 w-10 text-sm";
|
||||||
|
|
||||||
// Long-press state for mobile
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [longPressMenuOpen, setLongPressMenuOpen] = useState(false);
|
|
||||||
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const touchMoved = useRef(false);
|
const touchMoved = useRef(false);
|
||||||
|
|
||||||
|
const openMenu = useCallback(() => {
|
||||||
|
setMenuOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleContextMenu = useCallback((event: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setMenuOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleTouchStart = useCallback(() => {
|
const handleTouchStart = useCallback(() => {
|
||||||
touchMoved.current = false;
|
touchMoved.current = false;
|
||||||
longPressTimer.current = setTimeout(() => {
|
longPressTimer.current = setTimeout(() => {
|
||||||
if (!touchMoved.current) {
|
if (!touchMoved.current) {
|
||||||
setLongPressMenuOpen(true);
|
openMenu();
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}, []);
|
}, [openMenu]);
|
||||||
|
|
||||||
const handleTouchMove = useCallback(() => {
|
const handleTouchMove = useCallback(() => {
|
||||||
touchMoved.current = true;
|
touchMoved.current = true;
|
||||||
|
|
@ -120,12 +124,26 @@ export function SearchSpaceAvatar({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const avatarButton = (
|
const avatarButton = (withMenuHandlers = false) => (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onPointerDown={
|
||||||
|
withMenuHandlers
|
||||||
|
? (event) => {
|
||||||
|
if (event.button === 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onContextMenu={withMenuHandlers ? handleContextMenu : undefined}
|
||||||
|
onTouchStart={withMenuHandlers ? handleTouchStart : undefined}
|
||||||
|
onTouchMove={withMenuHandlers ? handleTouchMove : undefined}
|
||||||
|
onTouchEnd={withMenuHandlers ? handleTouchEnd : undefined}
|
||||||
|
onTouchCancel={withMenuHandlers ? handleTouchEnd : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative rounded-lg font-semibold text-white transition-all select-none",
|
"relative rounded-lg font-semibold text-white transition-all select-none",
|
||||||
"hover:text-white hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
"hover:text-white hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||||
|
|
@ -173,73 +191,41 @@ export function SearchSpaceAvatar({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
// If delete or settings handlers are provided, wrap with context menu
|
// If delete or settings handlers are provided, expose them through a dropdown menu.
|
||||||
if (onDelete || onSettings) {
|
if (onDelete || onSettings) {
|
||||||
// Mobile: use long-press triggered DropdownMenu
|
const trigger = (
|
||||||
if (disableTooltip) {
|
|
||||||
return (
|
|
||||||
<DropdownMenu open={longPressMenuOpen} onOpenChange={setLongPressMenuOpen}>
|
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div
|
{avatarButton(true)}
|
||||||
className="inline-block"
|
|
||||||
onTouchStart={handleTouchStart}
|
|
||||||
onTouchMove={handleTouchMove}
|
|
||||||
onTouchEnd={handleTouchEnd}
|
|
||||||
onTouchCancel={handleTouchEnd}
|
|
||||||
>
|
|
||||||
{avatarButton}
|
|
||||||
</div>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>{menuItems}</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Desktop: use right-click ContextMenu + Tooltip
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
||||||
|
{disableTooltip ? (
|
||||||
|
trigger
|
||||||
|
) : (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
|
||||||
<ContextMenuTrigger asChild>
|
|
||||||
<div className="inline-block">{avatarButton}</div>
|
|
||||||
</ContextMenuTrigger>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
{tooltipContent}
|
{tooltipContent}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ContextMenuContent>
|
|
||||||
{onSettings && (
|
|
||||||
<ContextMenuItem onClick={onSettings}>
|
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
|
||||||
{tCommon("settings")}
|
|
||||||
</ContextMenuItem>
|
|
||||||
)}
|
)}
|
||||||
{onDelete && isOwner && (
|
<DropdownMenuContent side="right" align="start">
|
||||||
<ContextMenuItem onClick={onDelete}>
|
{menuItems}
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
</DropdownMenuContent>
|
||||||
{tCommon("delete")}
|
</DropdownMenu>
|
||||||
</ContextMenuItem>
|
|
||||||
)}
|
|
||||||
{onDelete && !isOwner && (
|
|
||||||
<ContextMenuItem onClick={onDelete}>
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
|
||||||
{t("leave")}
|
|
||||||
</ContextMenuItem>
|
|
||||||
)}
|
|
||||||
</ContextMenuContent>
|
|
||||||
</ContextMenu>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No context menu needed
|
// No context menu needed
|
||||||
if (disableTooltip) {
|
if (disableTooltip) {
|
||||||
return avatarButton;
|
return avatarButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>{avatarButton}</TooltipTrigger>
|
<TooltipTrigger asChild>{avatarButton()}</TooltipTrigger>
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
{tooltipContent}
|
{tooltipContent}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue