Files
router-lists-ui/frontend/src/components/ContextMenu.jsx
T

142 lines
4.1 KiB
React

import { useState, useEffect, useRef } from 'react'
import { IconDotsVertical } from '@tabler/icons-react'
/**
* ContextMenu - компонент контекстного меню для действий
* Поддерживает правый клик и dropdown меню
*/
function ContextMenu({
items, // Array of { label, icon, onClick, variant, divider }
children, // Trigger element
align = 'right' // 'left' | 'right'
}) {
const [isOpen, setIsOpen] = useState(false)
const [position, setPosition] = useState({ x: 0, y: 0 })
const [useContextPosition, setUseContextPosition] = useState(false)
const menuRef = useRef(null)
const triggerRef = useRef(null)
const handleContextMenu = (e) => {
e.preventDefault()
setPosition({ x: e.clientX, y: e.clientY })
setUseContextPosition(true)
setIsOpen(true)
}
const handleClick = () => {
if (triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect()
setPosition({
x: align === 'right' ? rect.right : rect.left,
y: rect.bottom
})
setUseContextPosition(false)
setIsOpen(!isOpen)
}
}
const handleItemClick = (onClick) => {
setIsOpen(false)
if (onClick) {
onClick()
}
}
// Закрытие при клике вне меню
useEffect(() => {
const handleClickOutside = (e) => {
if (menuRef.current && !menuRef.current.contains(e.target) &&
triggerRef.current && !triggerRef.current.contains(e.target)) {
setIsOpen(false)
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}
}, [isOpen])
return (
<div
ref={triggerRef}
className="context-menu-trigger"
onContextMenu={handleContextMenu}
style={{ display: 'inline-block' }}
>
{children ? (
<div onClick={handleClick}>
{children}
</div>
) : (
<button
className="btn btn-ghost-secondary btn-icon btn-sm"
onClick={handleClick}
aria-label="Открыть меню"
title="Показать действия"
>
<IconDotsVertical size={18} />
</button>
)}
{isOpen && (
<>
<div
ref={menuRef}
className="dropdown-menu show"
style={{
position: 'fixed',
left: useContextPosition ? `${position.x}px` : align === 'right' ? 'auto' : `${position.x}px`,
right: useContextPosition ? 'auto' : align === 'right' ? `${window.innerWidth - position.x}px` : 'auto',
top: `${position.y}px`,
zIndex: 9999,
minWidth: '180px'
}}
>
{items.map((item, index) => {
if (item.divider) {
return <div key={index} className="dropdown-divider"></div>
}
const Icon = item.icon
const variantClass = item.variant === 'danger' ? 'text-danger' : ''
return (
<button
key={index}
className={`dropdown-item ${variantClass}`}
onClick={() => handleItemClick(item.onClick)}
disabled={item.disabled}
>
{Icon && <Icon size={18} className="me-2" />}
{item.label}
{item.shortcut && (
<kbd className="ms-auto">{item.shortcut}</kbd>
)}
</button>
)
})}
</div>
{/* Backdrop для мобильных */}
<div
className="dropdown-backdrop"
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 9998
}}
onClick={() => setIsOpen(false)}
></div>
</>
)}
</div>
)
}
export default ContextMenu