ui-nyla/src/components/Sidebar/SidebarItem.tsx

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;