201 lines
5.7 KiB
TypeScript
201 lines
5.7 KiB
TypeScript
import React, { useCallback, useState } from 'react';
|
|
import cx from 'classnames';
|
|
import Link from 'next/link';
|
|
import {
|
|
Menu,
|
|
MenuItem,
|
|
MenuSection,
|
|
VersionMetadata,
|
|
} from '@nrwl/nx-dev/data-access-documents';
|
|
import { useRouter } from 'next/router';
|
|
import { Selector } from '@nrwl/nx-dev/ui/common';
|
|
|
|
export interface SidebarProps {
|
|
menu: Menu;
|
|
version: VersionMetadata;
|
|
versionList: VersionMetadata[];
|
|
flavorList: any[];
|
|
flavor: any;
|
|
navIsOpen?: boolean;
|
|
}
|
|
|
|
// Exported for testing
|
|
export function createNextPath(
|
|
version: string,
|
|
flavor: string,
|
|
currentPath: string
|
|
): string {
|
|
const genericPath = currentPath.split('/').slice(3).join('/');
|
|
return `/${version}/${flavor}/${genericPath}`;
|
|
}
|
|
|
|
export function Sidebar({
|
|
flavor,
|
|
flavorList,
|
|
version,
|
|
versionList,
|
|
menu,
|
|
navIsOpen,
|
|
}: SidebarProps) {
|
|
const router = useRouter();
|
|
return (
|
|
<div
|
|
data-testid="sidebar"
|
|
className={cx(
|
|
'fixed z-40 inset-0 flex-none h-full bg-black bg-opacity-25 w-full lg:bg-white lg:static lg:h-auto lg:overflow-y-visible lg:pt-o lg:w-64 lg:block border-r border-gray-50',
|
|
!navIsOpen && 'hidden',
|
|
navIsOpen && 'block'
|
|
)}
|
|
>
|
|
<div
|
|
data-testid="navigation-wrapper"
|
|
className="h-full overflow-y-auto scrolling-touch lg:h-auto lg:block lg:relative lg:sticky lg:bg-transparent overflow-auto lg:top-18 bg-white mr-24 lg:mr-0 px-2 sm:pr-4 xl:pr-6"
|
|
>
|
|
<div className="hidden lg:block h-12 pointer-events-none absolute inset-x-0 z-10 bg-gradient-to-b from-white" />
|
|
<div className="px-1 pt-6 sm:px-3 xl:px-5 lg:pt-10">
|
|
<Selector
|
|
data={versionList.map((version) => ({
|
|
label: version.name,
|
|
value: version.id,
|
|
}))}
|
|
selected={{ label: version.name, value: version.id }}
|
|
onChange={(item) =>
|
|
router.push(
|
|
createNextPath(item.value, flavor.value, router.asPath)
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="px-1 pt-3 sm:px-3 xl:px-5">
|
|
<Selector
|
|
data={flavorList}
|
|
selected={flavor}
|
|
onChange={(item) =>
|
|
router.push(createNextPath(version.id, item.value, router.asPath))
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="px-1 py-6 sm:px-3 xl:px-5 h-1 w-full border-b border-gray-50" />
|
|
<nav
|
|
id="nav"
|
|
data-testid="navigation"
|
|
className="px-1 pt-1 font-medium text-base sm:px-3 xl:px-5 lg:text-sm pb-10 lg:pb-14 sticky?lg:h-(screen-18)"
|
|
>
|
|
{menu.sections.map((section) => (
|
|
<SidebarSection key={section.id} section={section} />
|
|
))}
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SidebarSection({ section }: { section: MenuSection }) {
|
|
return (
|
|
<>
|
|
{section.hideSectionHeader ? null : (
|
|
<h4
|
|
data-testid={`section-h4:${section.id}`}
|
|
className="mt-8 text-lg font-bold border-b border-gray-50 border-solid"
|
|
>
|
|
{section.name}
|
|
</h4>
|
|
)}
|
|
<ul>
|
|
<li className="mt-2">
|
|
{section.itemList.map((item) => (
|
|
<SidebarSectionItems key={item.id} item={item} />
|
|
))}
|
|
</li>
|
|
</ul>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function SidebarSectionItems({ item }: { item: MenuItem }) {
|
|
const router = useRouter();
|
|
const [collapsed, setCollapsed] = useState(!item.disableCollapsible);
|
|
|
|
const handleCollapseToggle = useCallback(() => {
|
|
if (!item.disableCollapsible) {
|
|
setCollapsed(!collapsed);
|
|
}
|
|
}, [collapsed, setCollapsed, item]);
|
|
|
|
function withoutAnchors(linkText: string): string {
|
|
return linkText?.includes('#')
|
|
? linkText.substring(0, linkText.indexOf('#'))
|
|
: linkText;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<h5
|
|
data-testid={`section-h5:${item.id}`}
|
|
className={cx(
|
|
'flex py-2',
|
|
'uppercase tracking-wide font-semibold text-sm lg:text-xs text-gray-900',
|
|
item.disableCollapsible ? 'cursor-text' : 'cursor-pointer'
|
|
)}
|
|
onClick={handleCollapseToggle}
|
|
>
|
|
{item.name}
|
|
{item.disableCollapsible ? null : (
|
|
<CollapsibleIcon isCollapsed={collapsed} />
|
|
)}
|
|
</h5>
|
|
<ul className={cx('mb-6', collapsed ? 'hidden' : '')}>
|
|
{item.itemList.map((item) => {
|
|
const isActiveLink = item.path === withoutAnchors(router?.asPath);
|
|
return (
|
|
<li key={item.id} data-testid={`section-li:${item.id}`}>
|
|
<Link href={item.path}>
|
|
<a
|
|
className={cx(
|
|
'py-1 transition-colors duration-200 relative block text-gray-500 hover:text-gray-900'
|
|
)}
|
|
>
|
|
{isActiveLink ? (
|
|
<span className="rounded-md absolute h-full w-1 -right-2 sm:-right-4 top-0 bg-blue-nx-base" />
|
|
) : null}
|
|
<span
|
|
className={cx('relative', {
|
|
'text-gray-900': isActiveLink,
|
|
})}
|
|
>
|
|
{item.name}
|
|
</span>
|
|
</a>
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function CollapsibleIcon({ isCollapsed }: { isCollapsed: boolean }) {
|
|
return (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className={cx(
|
|
'transition-all h-3.5 w-3.5 text-gray-500',
|
|
!isCollapsed && 'transform rotate-90'
|
|
)}
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d={'M9 5l7 7-7 7'}
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export default Sidebar;
|