Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions app/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"use client";

import { useEffect, useRef, useState } from "react";
import type { ElementType } from "react";

export interface ContextMenuAction {
label: string;
icon: ElementType;
onClick: () => void;
danger?: boolean;
disabled?: boolean;
}

interface ContextMenuProps {
position: { x: number; y: number };
actions: ContextMenuAction[];
onClose: () => void;
}

export default function ContextMenu({ position, actions, onClose }: ContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
const [menuPosition, setMenuPosition] = useState(position);

useEffect(() => {
const adjustPosition = () => {
const menuWidth = menuRef.current?.offsetWidth ?? 200;
const menuHeight = menuRef.current?.offsetHeight ?? actions.length * 40;

let top = position.y;
let left = position.x;

if (top + menuHeight > window.innerHeight) {
top = Math.max(0, window.innerHeight - menuHeight - 8);
}
if (left + menuWidth > window.innerWidth) {
left = Math.max(0, window.innerWidth - menuWidth - 8);
}

setMenuPosition({ x: left, y: top });
};

adjustPosition();
}, [position, actions.length]);

useEffect(() => {
const handleScroll = () => onClose();
const handleResize = () => onClose();

window.addEventListener("scroll", handleScroll, true);
window.addEventListener("resize", handleResize);

return () => {
window.removeEventListener("scroll", handleScroll, true);
window.removeEventListener("resize", handleResize);
};
}, [onClose]);

return (
<div
ref={menuRef}
className="fixed z-[999] w-52 rounded-lg border border-gray-200 bg-white shadow-lg"
style={{ top: menuPosition.y, left: menuPosition.x }}
role="menu"
onClick={(e) => e.stopPropagation()}
onContextMenu={(e) => e.preventDefault()}
>
{actions.map((action) => {
const Icon = action.icon;
return (
<button
key={action.label}
type="button"
onClick={(e) => {
e.stopPropagation();
if (action.disabled) return;
action.onClick();
}}
className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm text-left transition-colors ${
action.danger
? "text-red-600 hover:bg-red-50"
: action.disabled
? "text-gray-400 cursor-not-allowed"
: "text-gray-700 hover:bg-gray-100"
}`}
role="menuitem"
disabled={action.disabled}
>
<Icon
className={`h-5 w-5 ${
action.danger ? "text-red-500" : action.disabled ? "text-gray-300" : "text-gray-500"
}`}
/>
<span>{action.label}</span>
</button>
);
})}
</div>
);
}


12 changes: 12 additions & 0 deletions app/components/FileItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface FileItemProps {
onDownload?: (file: FileItemData) => void;
onDelete?: (file: FileItemData) => void;
isSelected?: boolean;
onContextMenu?: (file: FileItemData, event: React.MouseEvent) => void;
}

export default function FileItem({
Expand All @@ -42,6 +43,7 @@ export default function FileItem({
onDownload,
onDelete,
isSelected = false,
onContextMenu,
}: FileItemProps) {
const [isHovered, setIsHovered] = useState(false);
const clickCountRef = useRef(0);
Expand Down Expand Up @@ -128,6 +130,11 @@ export default function FileItem({
role="button"
tabIndex={0}
aria-label={`${file.type === "folder" ? "Folder" : "File"}: ${file.name}`}
onContextMenu={(event) => {
event.preventDefault();
event.stopPropagation();
onContextMenu?.(file, event);
}}
>
{isHovered && (
<FileItemMenu
Expand Down Expand Up @@ -164,6 +171,11 @@ export default function FileItem({
role="button"
tabIndex={0}
aria-label={`${file.type === "folder" ? "Folder" : "File"}: ${file.name}`}
onContextMenu={(event) => {
event.preventDefault();
event.stopPropagation();
onContextMenu?.(file, event);
}}
>
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center sm:h-10 sm:w-10">
{getFileIcon(file.type, file.mimeType)}
Expand Down
4 changes: 4 additions & 0 deletions app/components/FileList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface FileListProps {
onFileDownload?: (file: FileItemData) => void;
onFileDelete?: (file: FileItemData) => void;
selectedFileIds: string[];
onFileContextMenu?: (file: FileItemData, event: React.MouseEvent) => void;
}

const VIEW_STORAGE_KEY = "solid-file-manager-view";
Expand All @@ -33,6 +34,7 @@ export default function FileList({
onFileDownload,
onFileDelete,
selectedFileIds,
onFileContextMenu,
}: FileListProps) {
const [view, setView] = useState<"grid" | "list">(() => {
if (typeof window === "undefined") return "list";
Expand Down Expand Up @@ -72,6 +74,7 @@ export default function FileList({
onDownload={onFileDownload}
onDelete={onFileDelete}
isSelected={selectedFileIds.includes(file.id)}
onContextMenu={onFileContextMenu}
/>
))}
</div>
Expand All @@ -91,6 +94,7 @@ export default function FileList({
onDownload={onFileDownload}
onDelete={onFileDelete}
isSelected={selectedFileIds.includes(file.id)}
onContextMenu={onFileContextMenu}
/>
))}
</div>
Expand Down
Loading