142 lines
3.4 KiB
TypeScript
142 lines
3.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import styled, { css, type AnyStyledComponent } from 'styled-components';
|
|
import _ from 'lodash';
|
|
import { matchPath, NavLink, useLocation } from 'react-router-dom';
|
|
|
|
import { type MenuItem } from '@/constants/menus';
|
|
|
|
import { Root, List, Item, Sub, Link } from '@radix-ui/react-navigation-menu';
|
|
|
|
import { popoverMixins } from '@/styles/popoverMixins';
|
|
import { Collapsible } from './Collapsible';
|
|
|
|
type ElementProps<MenuItemValue extends string> = {
|
|
items: MenuItem<MenuItemValue>[];
|
|
onSelectItem?: (value: MenuItemValue) => void;
|
|
};
|
|
|
|
const NavItem = <MenuItemValue extends string>({
|
|
value,
|
|
label,
|
|
href,
|
|
onSelect,
|
|
subitems,
|
|
|
|
...props
|
|
}: MenuItem<MenuItemValue>) => {
|
|
const location = useLocation();
|
|
|
|
return !href ? null : (
|
|
<Item
|
|
asChild
|
|
key={value}
|
|
value={value}
|
|
>
|
|
<Link
|
|
asChild
|
|
active={!!matchPath(href, location.pathname)}
|
|
onSelect={() => onSelect?.(value)}
|
|
>
|
|
<NavLink
|
|
to={href}
|
|
{...props}
|
|
>
|
|
{label}
|
|
</NavLink>
|
|
</Link>
|
|
</Item>
|
|
);
|
|
};
|
|
|
|
export const CollapsibleNavigationMenu = <MenuItemValue extends string>({
|
|
onSelectItem,
|
|
items,
|
|
}: ElementProps<MenuItemValue>) => {
|
|
const { pathname } = useLocation();
|
|
const [expandedKey, setExpandedKey] = useState('');
|
|
|
|
useEffect(() => {
|
|
const itemWithActiveSubitem = items.find((item) =>
|
|
item.subitems?.some((subitem) => matchPath(subitem.href ?? '', pathname))
|
|
);
|
|
if (itemWithActiveSubitem) {
|
|
setExpandedKey(itemWithActiveSubitem.value);
|
|
}
|
|
}, [pathname]);
|
|
|
|
return (
|
|
<Root orientation="vertical">
|
|
<Styled.List>
|
|
{items.map((item) =>
|
|
!item.subitems ? (
|
|
<Styled.NavItem
|
|
key={item.value}
|
|
onSelect={onSelectItem}
|
|
{...item}
|
|
/>
|
|
) : (
|
|
<Collapsible
|
|
key={item.value}
|
|
open={expandedKey === item.value}
|
|
onOpenChange={(open) => {
|
|
setExpandedKey(!open ? '' : item.value);
|
|
}}
|
|
label={
|
|
<Styled.CollapsibleItem value={item.value}>{item.label}</Styled.CollapsibleItem>
|
|
}
|
|
transitionDuration={0.2}
|
|
>
|
|
<Styled.Sub defaultValue={item.subitems?.[0]}>
|
|
{item.subitems.map((subitem: MenuItem<MenuItemValue>) => (
|
|
<Styled.NavItem
|
|
key={subitem.value}
|
|
onSelect={onSelectItem}
|
|
{...subitem}
|
|
/>
|
|
))}
|
|
</Styled.Sub>
|
|
</Collapsible>
|
|
)
|
|
)}
|
|
</Styled.List>
|
|
</Root>
|
|
);
|
|
};
|
|
|
|
const Styled: Record<string, AnyStyledComponent> = {};
|
|
|
|
const navItemStyle = css`
|
|
${popoverMixins.item}
|
|
--item-padding: 0.5em 0.75em;
|
|
--item-radius: 0.5rem;
|
|
--item-checked-backgroundColor: var(--color-layer-1);
|
|
--item-checked-textColor: var(--color-text-0);
|
|
`;
|
|
|
|
Styled.List = styled(List)`
|
|
gap: 0.5rem;
|
|
`;
|
|
|
|
Styled.CollapsibleItem = styled(Item)`
|
|
${navItemStyle}
|
|
--item-padding: 0;
|
|
`;
|
|
|
|
Styled.Sub = styled(Sub)`
|
|
margin: -0.25rem 0.5rem 0;
|
|
padding-left: 0.5em;
|
|
border-left: solid var(--border-width) var(--color-border);
|
|
|
|
font-size: 0.92em;
|
|
`;
|
|
|
|
Styled.NavItem = styled(NavItem)`
|
|
${navItemStyle}
|
|
margin: 0.25em 0;
|
|
justify-content: flex-start;
|
|
|
|
${Styled.Sub} & {
|
|
--item-padding: 0.5em 0.7em;
|
|
}
|
|
`;
|