diff --git a/apps/site/components/withSidebar.tsx b/apps/site/components/withSidebar.tsx index 91866828ac934..b6d17fc62fa96 100644 --- a/apps/site/components/withSidebar.tsx +++ b/apps/site/components/withSidebar.tsx @@ -9,7 +9,7 @@ import { useClientContext, useScrollToElement } from '#site/hooks/client'; import { useSiteNavigation } from '#site/hooks/generic'; import { useRouter, usePathname } from '#site/navigation.mjs'; -import type { NavigationKeys } from '#site/types'; +import type { FormattedMessage, NavigationKeys } from '#site/types'; import type { RichTranslationValues } from 'next-intl'; import type { FC } from 'react'; @@ -18,6 +18,27 @@ type WithSidebarProps = { context?: Record; }; +type MappedItem = { + label: FormattedMessage; + link: string; + target?: string; + items?: Array<[string, MappedItem]>; +}; + +type SidebarMappedEntry = { + label: FormattedMessage; + link: string; + target?: string; + items?: Array; +}; + +const mapItem = ([, item]: [string, MappedItem]): SidebarMappedEntry => ({ + label: item.label, + link: item.link, + target: item.target, + items: item.items ? item.items.map(mapItem) : [], +}); + const WithSidebar: FC = ({ navKeys, context, ...props }) => { const { getSideNavigation } = useSiteNavigation(); const pathname = usePathname()!; @@ -34,9 +55,9 @@ const WithSidebar: FC = ({ navKeys, context, ...props }) => { // If there's only a single navigation key, use its sub-items // as our navigation. (navKeys.length === 1 ? sideNavigation[0][1].items : sideNavigation).map( - ([, { label, items }]) => ({ + ([, { label, items }]: [string, MappedItem]) => ({ groupName: label, - items: items.map(([, item]) => item), + items: items ? items.map(mapItem) : [], }) ); diff --git a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.module.css b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.module.css index 0cecc21978828..52e031715126a 100644 --- a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.module.css +++ b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.module.css @@ -21,4 +21,51 @@ text-neutral-800 dark:text-neutral-600; } + + .subGroup { + @apply flex + w-full + flex-col + gap-1; + } + + .summary { + @apply flex + cursor-pointer + items-center + justify-between + rounded-md + px-2 + py-1 + text-sm + font-semibold + text-neutral-800 + select-none + hover:bg-neutral-100 + dark:text-neutral-200 + hover:dark:bg-neutral-900; + + list-style: none; + + &::-webkit-details-marker { + display: none; + } + } + + .subGroup[open] .summary { + @apply text-green-600 + dark:text-green-400; + } + + .subItemList { + @apply mt-1 + ml-2 + flex + flex-col + gap-1 + border-l + border-neutral-200 + pl-2 + dark:border-neutral-800; + } } diff --git a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.stories.tsx b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.stories.tsx index 898e61bec7fe2..cca25d7f9c34c 100644 --- a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.stories.tsx +++ b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.stories.tsx @@ -33,4 +33,27 @@ export const EmptyGroup: Story = { }, }; +export const NestedGroup: Story = { + args: { + groupName: 'Nested Group', + pathname: '/nested/folder-b/leaf-2', + items: [ + { label: 'Flat Item', link: '/nested/flat' }, + { + label: 'Folder A', + link: '/nested/folder-a', + items: [{ label: 'Leaf A.1', link: '/nested/folder-a/leaf-1' }], + }, + { + label: 'Folder B', + link: '/nested/folder-b', + items: [ + { label: 'Leaf B.1', link: '/nested/folder-b/leaf-1' }, + { label: 'Leaf B.2 (Active)', link: '/nested/folder-b/leaf-2' }, + ], + }, + ], + }, +}; + export default { component: SidebarGroup } as Meta; diff --git a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx index e156939474058..edff8979c9d3e 100644 --- a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx +++ b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx @@ -7,27 +7,73 @@ import type { ComponentProps, FC } from 'react'; import styles from './index.module.css'; +type SidebarItemType = Omit< + ComponentProps, + 'as' | 'pathname' +> & { + items?: Array; +}; + type SidebarGroupProps = { groupName: FormattedMessage; - items: Array, 'as' | 'pathname'>>; + items: Array; as?: LinkLike; pathname?: string; - className: string; + className?: string; +}; + +const hasActivePath = ( + items: Array, + pathname?: string +): boolean => { + return items.some( + item => + item.link === pathname || + (item.items && hasActivePath(item.items, pathname)) + ); +}; + +const renderItems = ( + items: Array, + props: { as?: LinkLike }, + pathname?: string +) => { + return items.map(({ label, link, items: subItems }) => { + if (subItems && subItems.length > 0) { + const isOpen = link === pathname || hasActivePath(subItems, pathname); + return ( +
  • +
    + {label} +
      + {renderItems(subItems, props, pathname)} +
    +
    +
  • + ); + } + return ( + + ); + }); }; const SidebarGroup: FC = ({ groupName, items, className, + pathname, ...props }) => (
    -
      - {items.map(({ label, link }) => ( - - ))} -
    +
      {renderItems(items, props, pathname)}
    ); diff --git a/packages/ui-components/src/Containers/Sidebar/index.tsx b/packages/ui-components/src/Containers/Sidebar/index.tsx index b41c83e1630da..af9bc3a2f5f9e 100644 --- a/packages/ui-components/src/Containers/Sidebar/index.tsx +++ b/packages/ui-components/src/Containers/Sidebar/index.tsx @@ -3,11 +3,25 @@ import { forwardRef } from 'react'; import WithNoScriptSelect from '#ui/Common/Select/NoScriptSelect'; import SidebarGroup from '#ui/Containers/Sidebar/SidebarGroup'; -import type { LinkLike } from '#ui/types'; +import type { FormattedMessage, LinkLike } from '#ui/types'; import type { ComponentProps, PropsWithChildren } from 'react'; import styles from './index.module.css'; +type SidebarItemType = { + label: FormattedMessage; + link: string; + items?: Array; +}; + +const flattenItems = ( + items: Array +): Array => { + return items.flatMap((item: SidebarItemType) => + item.items && item.items.length ? flattenItems(item.items) : [item] + ); +}; + type SidebarProps = { groups: Array< Pick, 'items' | 'groupName'> @@ -23,7 +37,9 @@ const SideBar = forwardRef>( ({ groups, pathname, title, onSelect, as, children, placeholder }, ref) => { const selectItems = groups.map(({ items, groupName }) => ({ label: groupName, - items: items.map(({ label, link }) => ({ value: link, label })), + items: flattenItems(items as Array).map( + ({ label, link }) => ({ value: link, label }) + ), })); const currentItem = selectItems