259 lines
No EOL
12 KiB
TypeScript
259 lines
No EOL
12 KiB
TypeScript
import React, { useEffect, useRef } from "react";
|
|
import { Link } from "react-router-dom";
|
|
import { IoIosArrowDown } from "react-icons/io";
|
|
|
|
|
|
import styles from './SidebarStyles/SidebarItem.module.css';
|
|
import SidebarSubmenu from "./SidebarSubmenu";
|
|
import { SidebarItemProps } from "./sidebarTypes";
|
|
|
|
const SidebarItem: React.FC<SidebarItemProps> = React.memo(({
|
|
item,
|
|
isOpen,
|
|
onToggle,
|
|
isActive,
|
|
isMinimized
|
|
}) => {
|
|
const Icon = item.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
const hasSubItems = item.submenu && item.submenu.length > 0;
|
|
const isDisabled = item.moduleEnabled === false;
|
|
const iconContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Fix SVG dimensions when minimized - react-icons uses 1em which can be invisible
|
|
useEffect(() => {
|
|
if (isMinimized && iconContainerRef.current) {
|
|
const wrapper = iconContainerRef.current;
|
|
const svg = wrapper.querySelector('svg');
|
|
if (svg) {
|
|
// Force explicit pixel dimensions
|
|
svg.setAttribute('width', '25');
|
|
svg.setAttribute('height', '25');
|
|
svg.style.cssText = 'width: 25px !important; height: 25px !important; display: block !important;';
|
|
|
|
// Get the actual color from parent li element
|
|
const parentLi = wrapper.closest('li');
|
|
const parentColor = parentLi ? window.getComputedStyle(parentLi).color : '#000000';
|
|
|
|
// Force color directly - use black for now to ensure visibility
|
|
const iconColor = '#000000'; // Force black for visibility
|
|
svg.style.setProperty('color', iconColor, 'important');
|
|
svg.style.setProperty('fill', iconColor, 'important');
|
|
svg.style.setProperty('stroke', iconColor, 'important');
|
|
svg.setAttribute('fill', iconColor);
|
|
svg.setAttribute('stroke', iconColor);
|
|
|
|
// Set fill/stroke on all paths - use black
|
|
const paths = svg.querySelectorAll('path');
|
|
paths.forEach(path => {
|
|
const originalFill = path.getAttribute('fill');
|
|
const originalStroke = path.getAttribute('stroke');
|
|
const strokeWidth = path.getAttribute('stroke-width');
|
|
|
|
if (originalFill === 'none' || (!originalFill && originalStroke)) {
|
|
path.removeAttribute('fill');
|
|
path.setAttribute('stroke', iconColor);
|
|
if (!strokeWidth || strokeWidth === '0') {
|
|
path.setAttribute('stroke-width', '2');
|
|
}
|
|
path.style.setProperty('stroke', iconColor, 'important');
|
|
path.style.setProperty('stroke-width', '2px', 'important');
|
|
path.style.setProperty('fill', 'none', 'important');
|
|
} else {
|
|
path.removeAttribute('fill');
|
|
path.setAttribute('fill', iconColor);
|
|
path.style.setProperty('fill', iconColor, 'important');
|
|
path.style.setProperty('stroke', iconColor, 'important');
|
|
}
|
|
});
|
|
|
|
// Debug: Check if wrapper is visible
|
|
const wrapperRect = wrapper.getBoundingClientRect();
|
|
const wrapperComputed = window.getComputedStyle(wrapper);
|
|
const button = wrapper.parentElement?.querySelector('button');
|
|
const buttonComputed = button ? window.getComputedStyle(button) : null;
|
|
const svgRect = svg.getBoundingClientRect();
|
|
const svgComputed = window.getComputedStyle(svg);
|
|
const firstPath = paths[0];
|
|
const pathComputed = firstPath ? window.getComputedStyle(firstPath) : null;
|
|
const parentLiComputed = parentLi ? window.getComputedStyle(parentLi) : null;
|
|
const parentMenu = wrapper.closest(`.${styles.menu}`);
|
|
const parentMenuComputed = parentMenu ? window.getComputedStyle(parentMenu) : null;
|
|
|
|
// Check what's actually at the icon position
|
|
const centerX = wrapperRect.left + wrapperRect.width / 2;
|
|
const centerY = wrapperRect.top + wrapperRect.height / 2;
|
|
const elementsAtPoint = document.elementsFromPoint(centerX, centerY);
|
|
const wrapperInElements = elementsAtPoint.includes(wrapper);
|
|
|
|
console.log(`[${item.name}] Icon wrapper check:`, {
|
|
hasSubItems,
|
|
wrapperVisible: wrapperComputed.visibility === 'visible',
|
|
wrapperOpacity: wrapperComputed.opacity,
|
|
wrapperZIndex: wrapperComputed.zIndex,
|
|
wrapperDisplay: wrapperComputed.display,
|
|
wrapperBackgroundColor: wrapperComputed.backgroundColor,
|
|
wrapperRect: wrapperRect ? `width: ${wrapperRect.width}, height: ${wrapperRect.height}, top: ${wrapperRect.top}, left: ${wrapperRect.left}` : 'no rect',
|
|
svgExists: !!svg,
|
|
svgRect: svgRect ? `width: ${svgRect.width}, height: ${svgRect.height}, top: ${svgRect.top}, left: ${svgRect.left}` : 'no rect',
|
|
svgDisplay: svgComputed.display,
|
|
svgVisibility: svgComputed.visibility,
|
|
svgOpacity: svgComputed.opacity,
|
|
svgColor: svgComputed.color,
|
|
pathsCount: paths.length,
|
|
firstPathFill: pathComputed?.fill,
|
|
firstPathOpacity: pathComputed?.opacity,
|
|
buttonExists: !!button,
|
|
buttonZIndex: buttonComputed?.zIndex,
|
|
buttonPosition: buttonComputed?.position,
|
|
buttonRect: button ? button.getBoundingClientRect() : null,
|
|
elementsAtIconCenter: elementsAtPoint.slice(0, 5).map(el => ({
|
|
tag: el.tagName,
|
|
class: el.className?.split(' ')[0] || 'no-class',
|
|
zIndex: window.getComputedStyle(el).zIndex
|
|
})),
|
|
wrapperInElements: wrapperInElements,
|
|
parentLiOverflow: parentLiComputed?.overflow,
|
|
parentLiClipPath: parentLiComputed?.clipPath,
|
|
parentMenuOverflow: parentMenuComputed?.overflow,
|
|
parentMenuClipPath: parentMenuComputed?.clipPath
|
|
});
|
|
} else {
|
|
console.error(`[${item.name}] SVG NOT FOUND in wrapper!`);
|
|
}
|
|
}
|
|
}, [isMinimized, isActive, item.name, hasSubItems]);
|
|
|
|
|
|
const toggleSubmenu = (e: React.MouseEvent) => {
|
|
if (isDisabled) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onToggle();
|
|
};
|
|
|
|
const handleLinkClick = (e: React.MouseEvent) => {
|
|
if (isDisabled) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
// If item has submenu, prevent navigation and only toggle submenu
|
|
if (hasSubItems) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onToggle();
|
|
return;
|
|
}
|
|
// Allow normal navigation for items without submenu
|
|
};
|
|
|
|
return (
|
|
<div className={`${styles.menu} ${isMinimized ? styles.minimized : ''} ${isDisabled ? styles.disabled : ''}`}>
|
|
<li
|
|
className={`${isActive ? styles.active : ""} ${isDisabled ? styles.disabledItem : ""}`}
|
|
data-item-name={item.name}
|
|
>
|
|
{/* Icon - always render, CSS handles positioning */}
|
|
{Icon && !isMinimized && (
|
|
<Icon
|
|
className={`${styles.icon} ${isDisabled ? styles.disabledIcon : ''}`}
|
|
/>
|
|
)}
|
|
|
|
{/* Text and arrow - hidden when minimized */}
|
|
{!isMinimized && (
|
|
<>
|
|
{hasSubItems ? (
|
|
// For items with submenu, make the entire area clickable to toggle
|
|
<button
|
|
onClick={toggleSubmenu}
|
|
className={`${styles.menuTextButton} ${isDisabled ? styles.disabledLink : ''}`}
|
|
aria-disabled={isDisabled}
|
|
title={isDisabled ? `${item.name} (Module disabled)` : `Toggle ${item.name} submenu`}
|
|
>
|
|
<span className={`${styles.menuText} ${isDisabled ? styles.disabledText : ''}`}>
|
|
{item.name}
|
|
</span>
|
|
<IoIosArrowDown className={`${styles.hassubmenu} ${isOpen ? styles.rotated : ''} ${isDisabled ? styles.disabledArrow : ''}`} />
|
|
</button>
|
|
) : (
|
|
// For items without submenu, use normal link
|
|
<>
|
|
<Link
|
|
to={isDisabled ? "#" : (item.link || "#")}
|
|
className={`${styles.menuTextLink} ${isDisabled ? styles.disabledLink : ''}`}
|
|
onClick={handleLinkClick}
|
|
aria-disabled={isDisabled}
|
|
title={isDisabled ? `${item.name} (Module disabled)` : item.name}
|
|
>
|
|
<span className={`${styles.menuText} ${isDisabled ? styles.disabledText : ''}`}>
|
|
{item.name}
|
|
</span>
|
|
</Link>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
|
|
{/* Icon for minimized state - render directly as child of li */}
|
|
{Icon && isMinimized && (
|
|
<div
|
|
ref={iconContainerRef}
|
|
style={{
|
|
position: 'absolute',
|
|
top: '50%',
|
|
left: '50%',
|
|
transform: 'translate(-50%, -50%)',
|
|
width: '25px',
|
|
height: '25px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
pointerEvents: 'none',
|
|
zIndex: 10
|
|
}}
|
|
>
|
|
<Icon
|
|
className={`${styles.icon} ${styles.iconMinimized} ${isDisabled ? styles.disabledIcon : ''}`}
|
|
size={25}
|
|
style={{
|
|
width: '25px',
|
|
height: '25px',
|
|
color: '#000000',
|
|
fill: '#000000'
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Clickable overlay for items without submenu */}
|
|
{isMinimized && !isDisabled && !hasSubItems && (
|
|
<Link
|
|
to={item.link || "#"}
|
|
className={styles.minimizedOverlay}
|
|
title={item.name}
|
|
onClick={handleLinkClick}
|
|
/>
|
|
)}
|
|
|
|
{/* Clickable overlay for items with submenu */}
|
|
{isMinimized && hasSubItems && !isDisabled && (
|
|
<button
|
|
onClick={toggleSubmenu}
|
|
className={styles.minimizedSubmenuToggle}
|
|
title={`Toggle ${item.name} submenu`}
|
|
aria-expanded={isOpen}
|
|
/>
|
|
)}
|
|
</li>
|
|
{hasSubItems && !isDisabled && <SidebarSubmenu item={item} isOpen={isOpen} isMinimized={isMinimized} />}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
SidebarItem.displayName = 'SidebarItem';
|
|
|
|
export default SidebarItem; |