Create ContextMenu component

This commit is contained in:
TheThomaas 2024-12-23 12:50:36 +01:00
parent e3088133f3
commit 60370fc39f
7 changed files with 157 additions and 0 deletions

View file

@ -0,0 +1,8 @@
.contextMenu {
position: absolute;
width: 200px;
background-color: #383838;
border-radius: 5px;
box-sizing: border-box;
padding: 0;
}

View file

@ -0,0 +1,48 @@
import { ReactNode, MouseEvent, memo } from 'react';
import Menu from '../Menu';
import useContextMenu from '@renderer/hooks/useContextMenu';
import MenuItem from '../MenuItem';
export interface Item {
label: string
onClick: () => void
show: boolean
}
interface Props {
children: ReactNode
items: Item[]
}
// TODO only one context menu can be opened
const ContextMenu = ({ children, items }: Props) => {
const { menuVisible, menuItems, menuPosition, showMenu, hideMenu } = useContextMenu();
const handleContextMenu = (event: MouseEvent) => {
showMenu(event, items);
};
return (
<div onContextMenu={handleContextMenu} onClick={hideMenu}>
{children}
<Menu
open={menuVisible}
onClose={hideMenu}
anchorReference="anchorPosition"
className="contextMenu"
anchorPosition={
menuVisible
? { top: menuPosition.y, left: menuPosition.x }
: { top: 0, left: 0 }
}
>
{menuItems.map(({ label, onClick, show }, i) => show && (
<MenuItem key={i} onClick={onClick}>
{label}
</MenuItem>
))}
</Menu>
</div>
);
};
export default memo(ContextMenu);

View file

@ -0,0 +1,50 @@
import { ReactNode } from "react"
interface AnchorPosition {
top: number
left: number
}
interface Props {
autoFocus?: boolean
children?: ReactNode
open: boolean
onClose?: (event, reason) => void
anchorReference: string
anchorPosition: AnchorPosition
className: string
}
const Menu = ({ autoFocus = true, children, open, onClose, anchorReference, anchorPosition, ...rest }: Props) => {
const handleListKeyDown = event => {
if (event.key === 'Tab') {
event.preventDefault();
if (onClose) {
onClose(event, 'tabKeyDown');
}
}
if (event.key === 'Escape') {
event.preventDefault();
if (onClose) {
onClose(event, 'escapeKeyDown');
}
}
};
return (
<>
{open &&
<ul
style={{ position: 'absolute', top: anchorPosition.top, left: anchorPosition.left, zIndex: 1000, backgroundColor: '#fff', color: 'black', border: '1px solid #ccc', boxShadow: '0 2px 10px rgba(0, 0, 0, 0.2)' }}
role="menu"
aria-labelledby="contextmenu-label"
onKeyDown={handleListKeyDown}
{...rest}
>
{children}
</ul>
}
</>
);
};
export default Menu;

View file

@ -0,0 +1,14 @@
import { ReactNode } from 'react';
interface Props {
children: ReactNode
onClick: () => void
}
const MenuItem = ({ children, ...rest }: Props) => {
return (
<li role="menuitem" tabIndex={-1} {...rest}>{children}</li>
);
};
export default MenuItem;

View file

@ -0,0 +1,37 @@
import { useState, useCallback, useEffect } from 'react';
const useContextMenu = () => {
const [menuVisible, setMenuVisible] = useState(false);
const [menuItems, setMenuItems] = useState([]);
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
const showMenu = useCallback((event, items) => {
event.preventDefault();
setMenuPosition({ x: event.pageX, y: event.pageY });
setMenuItems(items);
setMenuVisible(true);
}, []);
const hideMenu = useCallback(() => {
setMenuVisible(false);
}, []);
useEffect(() => {
const handleDocumentClick = (event) => {
// Logic to hide the context menu
hideMenu();
};
// Register the event listener
document.addEventListener('click', handleDocumentClick);
// Cleanup function to remove the event listener
return () => {
document.removeEventListener('click', handleDocumentClick);
};
}, [hideMenu]);
return { menuVisible, menuItems, menuPosition, showMenu, hideMenu };
};
export default useContextMenu;