mirror of
https://github.com/root-fr/jmap-webmail.git
synced 2026-10-02 16:00:31 +00:00
New features: - Keyboard shortcuts (j/k navigation, r/R/f for reply/forward, s star, e archive, # delete) - Drag-and-drop emails to mailbox folders with multi-select support - Right-click context menus on email list items with all actions - Mobile-responsive adaptive layout with hamburger menu navigation New files: - hooks/use-keyboard-shortcuts.ts - Global keyboard shortcut handling - hooks/use-email-drag.ts, hooks/use-mailbox-drop.ts - Native HTML5 DnD API - hooks/use-context-menu.ts, components/ui/context-menu.tsx - Context menu system - hooks/use-media-query.ts, stores/ui-store.ts - Responsive breakpoint handling - components/layout/mobile-header.tsx - Mobile navigation header - contexts/drag-drop-context.tsx - Drag-drop state management - components/keyboard-shortcuts-modal.tsx - Help modal (? key) - components/email/email-context-menu.tsx - Email-specific context menu Updated TODO.md with completed tasks and code audit summary
127 lines
3.1 KiB
TypeScript
127 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useCallback, useEffect, useRef } from "react";
|
|
|
|
interface Position {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
interface ContextMenuState<T> {
|
|
isOpen: boolean;
|
|
position: Position;
|
|
data: T | null;
|
|
}
|
|
|
|
interface UseContextMenuReturn<T> {
|
|
contextMenu: ContextMenuState<T>;
|
|
openContextMenu: (e: React.MouseEvent, data: T) => void;
|
|
closeContextMenu: () => void;
|
|
menuRef: React.RefObject<HTMLDivElement | null>;
|
|
}
|
|
|
|
const MENU_WIDTH = 200;
|
|
const MENU_HEIGHT = 320; // Approximate max height
|
|
|
|
export function useContextMenu<T>(): UseContextMenuReturn<T> {
|
|
const [contextMenu, setContextMenu] = useState<ContextMenuState<T>>({
|
|
isOpen: false,
|
|
position: { x: 0, y: 0 },
|
|
data: null,
|
|
});
|
|
|
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
const calculatePosition = useCallback((clientX: number, clientY: number): Position => {
|
|
const viewportWidth = window.innerWidth;
|
|
const viewportHeight = window.innerHeight;
|
|
|
|
let x = clientX;
|
|
let y = clientY;
|
|
|
|
// Adjust for right edge
|
|
if (x + MENU_WIDTH > viewportWidth - 10) {
|
|
x = viewportWidth - MENU_WIDTH - 10;
|
|
}
|
|
|
|
// Adjust for bottom edge
|
|
if (y + MENU_HEIGHT > viewportHeight - 10) {
|
|
y = viewportHeight - MENU_HEIGHT - 10;
|
|
}
|
|
|
|
// Ensure minimum position
|
|
x = Math.max(10, x);
|
|
y = Math.max(10, y);
|
|
|
|
return { x, y };
|
|
}, []);
|
|
|
|
const openContextMenu = useCallback((e: React.MouseEvent, data: T) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const position = calculatePosition(e.clientX, e.clientY);
|
|
|
|
setContextMenu({
|
|
isOpen: true,
|
|
position,
|
|
data,
|
|
});
|
|
}, [calculatePosition]);
|
|
|
|
const closeContextMenu = useCallback(() => {
|
|
setContextMenu((prev) => ({
|
|
...prev,
|
|
isOpen: false,
|
|
}));
|
|
}, []);
|
|
|
|
// Close on escape key, click outside, and scroll
|
|
useEffect(() => {
|
|
if (!contextMenu.isOpen) return;
|
|
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") {
|
|
closeContextMenu();
|
|
}
|
|
};
|
|
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
closeContextMenu();
|
|
}
|
|
};
|
|
|
|
const handleScroll = () => {
|
|
closeContextMenu();
|
|
};
|
|
|
|
const handleBlur = () => {
|
|
closeContextMenu();
|
|
};
|
|
|
|
// Add listeners with a slight delay to prevent immediate closing
|
|
const timeoutId = setTimeout(() => {
|
|
document.addEventListener("keydown", handleEscape);
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
document.addEventListener("scroll", handleScroll, true);
|
|
window.addEventListener("blur", handleBlur);
|
|
}, 0);
|
|
|
|
return () => {
|
|
clearTimeout(timeoutId);
|
|
document.removeEventListener("keydown", handleEscape);
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
document.removeEventListener("scroll", handleScroll, true);
|
|
window.removeEventListener("blur", handleBlur);
|
|
};
|
|
}, [contextMenu.isOpen, closeContextMenu]);
|
|
|
|
return {
|
|
contextMenu,
|
|
openContextMenu,
|
|
closeContextMenu,
|
|
menuRef,
|
|
};
|
|
}
|