breadcrumbs

This commit is contained in:
2025-10-31 17:43:00 +01:00
parent 1a1e9a80c3
commit 1fcd265eb9
4 changed files with 361 additions and 245 deletions
+85 -89
View File
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState, useId } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useId } from 'react';
import { createPortal } from 'react-dom';
import {
ChevronIcon,
TrashIcon,
@@ -17,6 +18,7 @@ import {
DesktopIcon,
} from '../ui/icons';
import PanelHeader from '../ui/PanelHeader';
import useFloatingMenu from '../ui/useFloatingMenu';
import { getTagColorStyle } from '../utils/colors';
@@ -282,44 +284,32 @@ const Sidebar = ({
onSearchClear?.();
}, [onSearchClear]);
const tenantButtonRef = useRef(null);
const tenantMenuRef = useRef(null);
const [tenantMenuOpen, setTenantMenuOpen] = useState(false);
const {
isOpen: tenantMenuOpen,
toggle: toggleTenantMenuFloating,
close: closeTenantMenu,
menuRef: tenantMenuRef,
menuStyle: tenantMenuStyle,
updatePosition: refreshTenantMenuPosition,
} = useFloatingMenu({
anchorRef: tenantButtonRef,
minWidth: 220,
offset: 6,
});
const showTenantList = tenants.length > 1;
const toggleTenantMenu = useCallback(() => {
const next = !tenantMenuOpen;
setTenantMenuOpen(next);
if (next && tenants.length === 0 && onSelectTenant) {
if (!tenantMenuOpen && tenants.length === 0 && onSelectTenant) {
onSelectTenant(null, { refreshOnly: true });
}
}, [tenantMenuOpen, tenants.length, onSelectTenant]);
toggleTenantMenuFloating();
}, [tenantMenuOpen, tenants.length, onSelectTenant, toggleTenantMenuFloating]);
useEffect(() => {
if (!tenantMenuOpen) {
return undefined;
if (tenantMenuOpen) {
refreshTenantMenuPosition();
}
const handlePointer = (event) => {
const menuNode = tenantMenuRef.current;
const buttonNode = tenantButtonRef.current;
if (!menuNode) return;
if (menuNode.contains(event.target)) return;
if (buttonNode && buttonNode.contains(event.target)) return;
setTenantMenuOpen(false);
};
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
setTenantMenuOpen(false);
}
};
document.addEventListener('mousedown', handlePointer);
document.addEventListener('touchstart', handlePointer);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('mousedown', handlePointer);
document.removeEventListener('touchstart', handlePointer);
document.removeEventListener('keydown', handleKeyDown);
};
}, [tenantMenuOpen]);
}, [tenantMenuOpen, refreshTenantMenuPosition, tenants.length]);
const handleTenantSelect = useCallback(
(tenant) => {
@@ -327,21 +317,21 @@ const Sidebar = ({
if (!targetId) {
return;
}
setTenantMenuOpen(false);
closeTenantMenu();
onSelectTenant?.(tenant);
},
[onSelectTenant],
[closeTenantMenu, onSelectTenant],
);
const handleLogoutFromMenu = useCallback(() => {
setTenantMenuOpen(false);
closeTenantMenu();
onLogout?.();
}, [onLogout]);
}, [closeTenantMenu, onLogout]);
const handleSettingsFromMenu = useCallback(() => {
setTenantMenuOpen(false);
closeTenantMenu();
onOpenSettings?.();
}, [onOpenSettings]);
}, [closeTenantMenu, onOpenSettings]);
const renderNodes = useCallback(
(ids, depth) =>
@@ -452,59 +442,65 @@ const Sidebar = ({
size={16}
/>
</button>
{tenantMenuOpen ? (
<div
className={`menu${showTenantList ? '' : ' menu--simple'}`}
ref={tenantMenuRef}
role="menu"
aria-label="Account menu"
>
{showTenantList ? (
<>
<div className="menu__heading">Switch tenant</div>
<div className="menu__list">
{tenants.map((tenant) => {
const tenantId = tenant?.id || null;
const isActive = tenantId === activeTenantId;
const tenantLabel = tenant?.name;
return (
<button
key={tenantId || tenantLabel}
type="button"
className={`menu__item${isActive ? ' active' : ''}`}
onClick={() => handleTenantSelect(tenant)}
role="menuitem"
>
<span className="menu__check-slot">
{isActive ? <CheckIcon size={16} /> : null}
</span>
<span className="menu__label">{tenantLabel}</span>
</button>
);
})}
{tenantMenuOpen
&& tenantMenuStyle
&& typeof document !== 'undefined'
? createPortal(
<div
className={`menu${showTenantList ? '' : ' menu--simple'}`}
ref={tenantMenuRef}
role="menu"
aria-label="Account menu"
style={tenantMenuStyle}
>
{showTenantList ? (
<>
<div className="menu__heading">Switch tenant</div>
<div className="menu__list">
{tenants.map((tenant) => {
const tenantId = tenant?.id || null;
const isActive = tenantId === activeTenantId;
const tenantLabel = tenant?.name;
return (
<button
key={tenantId || tenantLabel}
type="button"
className={`menu__item${isActive ? ' active' : ''}`}
onClick={() => handleTenantSelect(tenant)}
role="menuitem"
>
<span className="menu__check-slot">
{isActive ? <CheckIcon size={16} /> : null}
</span>
<span className="menu__label">{tenantLabel}</span>
</button>
);
})}
</div>
</>
) : null}
<div className="menu__footer">
<button
type="button"
className="menu__settings"
onClick={handleSettingsFromMenu}
>
<SettingsIcon size={16} />
Settings
</button>
<button
type="button"
className="menu__logout"
onClick={handleLogoutFromMenu}
>
<LogoutIcon size={16} />
Log out
</button>
</div>
</>
) : null}
<div className="menu__footer">
<button
type="button"
className="menu__settings"
onClick={handleSettingsFromMenu}
>
<SettingsIcon size={16} />
Settings
</button>
<button
type="button"
className="menu__logout"
onClick={handleLogoutFromMenu}
>
<LogoutIcon size={16} />
Log out
</button>
</div>
</div>
) : null}
</div>,
document.body,
)
: null}
</>
)}
actions={