Android_Retail/src/Components/Menu/PozoMenu.jsx

423 lines
14 KiB
React
Raw Normal View History

2026-02-06 14:27:37 +05:30
import { useState, useRef, useEffect } from 'react';
2026-01-27 18:27:29 +05:30
import { FaCaretRight } from "react-icons/fa";
import './PozoMenu.scss';
const PozoMenu = ({ items = [], mode = "vertical", style = {}, onClick, collapse = false, selectedKey: propSelectedKey, isBottomMenu = false }) => {
const [openKeys, setOpenKeys] = useState([]);
const [selectedKey, setSelectedKey] = useState('');
const [dropdownPositions, setDropdownPositions] = useState({});
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const menuRef = useRef();
// Update selected key when prop changes
useEffect(() => {
if (propSelectedKey !== undefined) {
setSelectedKey(propSelectedKey);
}
}, [propSelectedKey]);
// Close dropdown when clicking outside, window resize, or scroll
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setOpenKeys([]);
setDropdownPositions({});
}
};
const handleResize = () => {
// Update mobile detection
setIsMobile(window.innerWidth <= 768);
// Recalculate positions on window resize
if (openKeys.length > 0) {
setOpenKeys([]);
setDropdownPositions({});
}
};
const handleScroll = () => {
// 🎯 FIXED: Recalculate positions on scroll to maintain proper positioning
if (openKeys.length > 0) {
// Find all currently open menu elements and recalculate their positions
const newPositions = {};
openKeys.forEach(key => {
const element = document.querySelector(`[data-menu-key="${key}"]`);
if (element) {
const level = key.split('-').length - 1;
newPositions[key] = calculatePosition(element, level);
}
});
setDropdownPositions(prev => ({ ...prev, ...newPositions }));
}
};
document.addEventListener('mousedown', handleClickOutside);
window.addEventListener('resize', handleResize);
window.addEventListener('scroll', handleScroll, true); // Capture scroll events
return () => {
document.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('resize', handleResize);
window.removeEventListener('scroll', handleScroll, true);
};
}, [openKeys]);
// 🎯 FIXED: Smart positioning calculation - exactly like your image behavior
const calculatePosition = (element, level = 0) => {
const rect = element.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
// Dynamic dropdown dimensions - responsive
const dropdownWidth = isMobile
? Math.min(280, viewportWidth - 20)
: (level > 0 ? 220 : 250);
const dropdownHeight = Math.min(
isMobile ? viewportHeight * 0.5 : 400,
viewportHeight * 0.6
);
const gap = isMobile ? 2 : 4;
const edgePadding = isMobile ? 10 : 8;
// Calculate available space on both sides
const spaceRight = viewportWidth - rect.right;
const spaceLeft = rect.left;
const spaceNeeded = dropdownWidth + gap + edgePadding;
let position = {
top: rect.top + scrollY,
left: rect.right + gap + scrollX,
direction: 'right',
verticalDirection: 'down'
};
// 🎯 FIXED: HORIZONTAL POSITIONING - Exactly like your images
if (level === 0) {
console.log('🎯 Space Analysis:', {
spaceRight,
spaceLeft,
spaceNeeded,
elementPosition: { left: rect.left, right: rect.right }
});
if (spaceRight >= spaceNeeded) {
// Image 1: Enough space on right - open to RIGHT
position.left = rect.right + gap + scrollX;
position.direction = 'right';
console.log('✅ Opening RIGHT - space available');
} else if (spaceLeft >= spaceNeeded) {
// Image 2: No space on right, space on left - open to LEFT
position.left = rect.left - dropdownWidth - gap + scrollX;
position.direction = 'left';
2026-02-06 14:27:37 +05:30
2026-01-27 18:27:29 +05:30
} else {
// Edge case: Limited space on both sides
if (spaceRight >= spaceLeft) {
position.left = viewportWidth - dropdownWidth - edgePadding + scrollX;
position.direction = 'right-edge';
} else {
position.left = edgePadding + scrollX;
position.direction = 'left-edge';
}
console.log('⚠️ Edge positioning applied');
}
} else {
// Nested menu positioning
const parentKeys = Object.keys(dropdownPositions);
const parentDirection = parentKeys.length > 0
? dropdownPositions[parentKeys[level - 1]]?.direction || 'right'
: 'right';
if (parentDirection.includes('right')) {
if (spaceRight >= spaceNeeded) {
position.left = rect.right + gap + scrollX;
position.direction = 'right';
} else {
position.left = rect.left - dropdownWidth - gap + scrollX;
position.direction = 'left';
}
} else {
if (spaceLeft >= spaceNeeded) {
position.left = rect.left - dropdownWidth - gap + scrollX;
position.direction = 'left';
} else {
position.left = rect.right + gap + scrollX;
position.direction = 'right';
}
}
}
// 🎯 FIXED: VERTICAL POSITIONING
const spaceBelow = viewportHeight - rect.bottom;
const spaceAbove = rect.top;
if (isBottomMenu && level === 0) {
if (spaceAbove >= dropdownHeight + edgePadding) {
position.top = rect.top - dropdownHeight - gap + scrollY;
position.verticalDirection = 'up';
} else {
position.top = rect.bottom + gap + scrollY;
position.verticalDirection = 'down';
}
} else {
if (spaceBelow >= dropdownHeight + edgePadding) {
position.top = rect.top + scrollY;
position.verticalDirection = 'down';
} else if (spaceAbove >= dropdownHeight + edgePadding) {
position.top = rect.bottom - dropdownHeight + scrollY;
position.verticalDirection = 'up';
} else {
position.top = Math.max(edgePadding, (viewportHeight - dropdownHeight) / 2) + scrollY;
position.verticalDirection = 'center';
}
}
// 🎯 FIXED: Final boundary checks
position.left = Math.max(
edgePadding + scrollX,
Math.min(position.left, viewportWidth - dropdownWidth - edgePadding + scrollX)
);
position.top = Math.max(
edgePadding + scrollY,
Math.min(position.top, viewportHeight - dropdownHeight - edgePadding + scrollY)
);
// Store dimensions for reference
position.width = dropdownWidth;
position.height = dropdownHeight;
console.log('🎯 FIXED Position:', {
clicked: { top: rect.top, left: rect.left, right: rect.right, bottom: rect.bottom },
spaces: { right: spaceRight, left: spaceLeft, above: spaceAbove, below: spaceBelow },
calculated: position,
direction: position.direction
});
return position;
};
// Handle menu item click
const handleMenuClick = (item, event, parentKeys = []) => {
event.stopPropagation();
if (item.children && item.children.length > 0) {
// Has children - toggle submenu
const itemKey = item.key;
const isOpen = openKeys.includes(itemKey);
if (isOpen) {
// Close this submenu and all its children
setOpenKeys(prev => prev.filter(key => {
// Keep only keys that are not this item or its descendants
return !key.startsWith(itemKey) && key !== itemKey;
}));
setDropdownPositions(prev => {
const newPos = { ...prev };
Object.keys(newPos).forEach(key => {
if (key.startsWith(itemKey) || key === itemKey) {
delete newPos[key];
}
});
return newPos;
});
} else {
// Close all submenus at the same level or deeper
const currentPath = [...parentKeys, itemKey];
const newOpenKeys = openKeys.filter(key => {
// Keep only parent keys in the current path
return currentPath.some(pathKey => key === pathKey) ||
currentPath.every((pathKey, index) => {
const keyParts = key.split('-');
const pathParts = pathKey.split('-');
return index < keyParts.length && keyParts[index] === pathParts[index];
});
});
// Add current item to open keys
newOpenKeys.push(itemKey);
// Calculate position and add to dropdown positions
const position = calculatePosition(event.currentTarget, parentKeys.length);
2026-02-06 14:27:37 +05:30
2026-01-27 18:27:29 +05:30
setDropdownPositions(prev => {
// Remove positions for closed items
const newPos = {};
newOpenKeys.forEach(key => {
if (prev[key]) newPos[key] = prev[key];
});
newPos[itemKey] = position;
return newPos;
});
setOpenKeys(newOpenKeys);
}
} else {
// No children - select item and close all dropdowns
setSelectedKey(item.key);
setOpenKeys([]);
setDropdownPositions({});
// Trigger onClick callback if provided
if (onClick) {
onClick({
key: item.key,
keyPath: [...parentKeys, item.key],
item: item,
domEvent: event
});
}
}
};
// Check if item is selected
const isSelected = (key) => selectedKey === key;
// Check if item is open
const isOpen = (key) => openKeys.includes(key);
// Check if item is in active path (has selected child)
const isInActivePath = (item, parentKeys = []) => {
if (!item.children) return false;
const checkChildren = (children, currentPath) => {
return children.some(child => {
const childPath = [...currentPath, child.key];
if (child.key === selectedKey) return true;
if (child.children) {
return checkChildren(child.children, childPath);
}
return false;
});
};
return checkChildren(item.children, [...parentKeys, item.key]);
};
const menuRefs = useRef({});
const submenuRefs = useRef({});
const [positions, setPositions] = useState({});
const renderMenuItems = (items, parentKeys = [], level = 0) => {
return items.map((item) => {
if (!item) return null;
const hasChildren = item.children && item.children.length > 0;
const isItemSelected = isSelected(item.key);
const isItemOpen = isOpen(item.key);
const isItemInActivePath = isInActivePath(item, parentKeys);
const currentKeys = [...parentKeys, item.key];
return (
<div
key={item.key}
className="pozo-menu-item-wrapper"
ref={(el) => {
if (el) menuRefs.current[item.key] = el;
}}
>
<div
className={`pozo-menu-item ${isItemSelected ? 'selected' : ''} ${isItemOpen ? 'open' : ''} ${isItemInActivePath ? 'active-path' : ''} ${hasChildren ? 'has-children' : ''} ${item.type === 'group' ? 'group-item' : ''}`}
onClick={(e) => handleMenuClick(item, e, parentKeys)}
data-menu-key={item.key}
style={{ justifyContent: "flex-start" }}
>
{item.icon && (
<span className="pozo-menu-icon" style={{ fontSize: collapse ? "1.3rem" : "" }}>
{item.icon}
</span>
)}
{!collapse && <span className="pozo-menu-label">{item.label}</span>}
{hasChildren && !collapse && (
// <span className="pozo-menu-arrow"><FaCaretRight /></span>
<span className={`${isItemOpen ? "open " : ""}pozo-menu-arrow`}>
<FaCaretRight />
</span>
)}
</div>
{hasChildren && isItemOpen && !collapse && (
<div
ref={(el) => {
if (el) submenuRefs.current[item.key] = el;
}}
className={`pozo-dropdown-submenu ${level > 0 ? 'nested-submenu' : ''}`}
style={{
position: 'fixed',
top: (() => {
const defaultTop = positions[item.key]?.top || 0;
const submenuHeight =
submenuRefs.current[item.key]?.getBoundingClientRect()?.height || 0;
const screenHeight = window.innerHeight;
// ✅ If submenu would overflow screen bottom, adjust top
if (defaultTop + submenuHeight > screenHeight) {
return Math.max(
8, // padding from top
screenHeight - submenuHeight - 8
);
}
return defaultTop;
})(),
left: positions[item.key]?.left + 5 || 0,
// width: 220,
width: 200,
maxHeight: 400,
zIndex: 1000 + level
}}
>
<div className="pozo-submenu-content">
{renderMenuItems(item.children, currentKeys, level + 1)}
</div>
</div>
)}
</div>
);
});
};
// ✅ Compute positions after render
useEffect(() => {
const updated = {};
for (const [key, el] of Object.entries(menuRefs.current)) {
if (el && typeof el.getBoundingClientRect === "function") {
const rect = el.getBoundingClientRect();
updated[key] = {
top: rect.top + window.scrollY,
left: rect.right + window.scrollX
};
}
}
Object.entries(submenuRefs.current).forEach(([key, el]) => {
if (el) {
const height = el.getBoundingClientRect().height;
console.log(`Total submenu height for ${key}:`, height);
}
});
setPositions(updated);
}, [items, openKeys]);
return (
<div
className={`pozo-menu pozo-menu-${mode}`}
style={style}
>
<div className="pozo-menu-content" ref={menuRef}>
{renderMenuItems(items)}
</div>
</div>
);
};
export default PozoMenu;