import React, { useState, useEffect, useRef } from 'react'; import Icon from './Icon'; // Хелпер: если icon — строка (эмодзи или name), рендерим Icon. Если JSX — как есть. const renderItemIcon = (val) => { if (!val) return null; if (typeof val === 'string') { const isAscii = /^[\x20-\x7E_-]+$/.test(val); return isAscii ? : ; } return val; }; /** * HoverPlusMenu — компактная «+»-кнопка справа от элемента иерархии. * * Отображается только при hover родителя (`visible` пропс). Клик по «+» * раскрывает выпадающее меню действий вниз. * * Пример использования: *
* ...название... * addScript(obj) }, * { id: 'duplicate', label: 'Дублировать', icon: '📋', onClick: ... }, * { divider: true }, * { id: 'delete', label: 'Удалить', icon: '🗑', danger: true, onClick: ... }, * ]} * /> *
* * Props: * visible — если false, кнопка не рендерится (или невидима) * items — массив пунктов меню * align — 'right' (default) или 'left' */ const HoverPlusMenu = ({ visible, items = [], align = 'right', alwaysVisible = false }) => { const [open, setOpen] = useState(false); const wrapperRef = useRef(null); // Закрытие по клику снаружи useEffect(() => { if (!open) return; const onDoc = (e) => { if (wrapperRef.current && !wrapperRef.current.contains(e.target)) { setOpen(false); } }; // setTimeout чтобы текущий клик (по +) не закрыл сразу const t = setTimeout(() => document.addEventListener('mousedown', onDoc), 0); return () => { clearTimeout(t); document.removeEventListener('mousedown', onDoc); }; }, [open]); // Закрытие по Esc useEffect(() => { if (!open) return; const onKey = (e) => { if (e.key === 'Escape') setOpen(false); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [open]); if (!visible && !open && !alwaysVisible) return null; if (!items || items.length === 0) return null; return (
e.stopPropagation()} > {open && (
{items.map((it, i) => { if (it.divider) { return
; } return ( ); })}
)}
); }; export default HoverPlusMenu;