Skip to content

Commit aef1152

Browse files
VenkatVenkat
authored andcommitted
feat(client): add nested sections support with auto-expansion in sidebar
1 parent 9345c76 commit aef1152

File tree

4 files changed

+144
-12
lines changed

4 files changed

+144
-12
lines changed

apps/site/components/withSidebar.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useClientContext, useScrollToElement } from '#site/hooks/client';
99
import { useSiteNavigation } from '#site/hooks/generic';
1010
import { useRouter, usePathname } from '#site/navigation.mjs';
1111

12-
import type { NavigationKeys } from '#site/types';
12+
import type { FormattedMessage, NavigationKeys } from '#site/types';
1313
import type { RichTranslationValues } from 'next-intl';
1414
import type { FC } from 'react';
1515

@@ -18,6 +18,27 @@ type WithSidebarProps = {
1818
context?: Record<string, RichTranslationValues>;
1919
};
2020

21+
type MappedItem = {
22+
label: FormattedMessage;
23+
link: string;
24+
target?: string;
25+
items?: Array<[string, MappedItem]>;
26+
};
27+
28+
type SidebarMappedEntry = {
29+
label: FormattedMessage;
30+
link: string;
31+
target?: string;
32+
items?: Array<SidebarMappedEntry>;
33+
};
34+
35+
const mapItem = ([, item]: [string, MappedItem]): SidebarMappedEntry => ({
36+
label: item.label,
37+
link: item.link,
38+
target: item.target,
39+
items: item.items ? item.items.map(mapItem) : [],
40+
});
41+
2142
const WithSidebar: FC<WithSidebarProps> = ({ navKeys, context, ...props }) => {
2243
const { getSideNavigation } = useSiteNavigation();
2344
const pathname = usePathname()!;
@@ -34,9 +55,9 @@ const WithSidebar: FC<WithSidebarProps> = ({ navKeys, context, ...props }) => {
3455
// If there's only a single navigation key, use its sub-items
3556
// as our navigation.
3657
(navKeys.length === 1 ? sideNavigation[0][1].items : sideNavigation).map(
37-
([, { label, items }]) => ({
58+
([, { label, items }]: [string, MappedItem]) => ({
3859
groupName: label,
39-
items: items.map(([, item]) => item),
60+
items: items ? items.map(mapItem) : [],
4061
})
4162
);
4263

packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.module.css

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,51 @@
2121
text-neutral-800
2222
dark:text-neutral-600;
2323
}
24+
25+
.subGroup {
26+
@apply flex
27+
w-full
28+
flex-col
29+
gap-1;
30+
}
31+
32+
.summary {
33+
@apply flex
34+
cursor-pointer
35+
items-center
36+
justify-between
37+
rounded-md
38+
px-2
39+
py-1
40+
text-sm
41+
font-semibold
42+
text-neutral-800
43+
select-none
44+
hover:bg-neutral-100
45+
dark:text-neutral-200
46+
hover:dark:bg-neutral-900;
47+
48+
list-style: none;
49+
50+
&::-webkit-details-marker {
51+
display: none;
52+
}
53+
}
54+
55+
.subGroup[open] .summary {
56+
@apply text-green-600
57+
dark:text-green-400;
58+
}
59+
60+
.subItemList {
61+
@apply mt-1
62+
ml-2
63+
flex
64+
flex-col
65+
gap-1
66+
border-l
67+
border-neutral-200
68+
pl-2
69+
dark:border-neutral-800;
70+
}
2471
}

packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,75 @@ import type { ComponentProps, FC } from 'react';
77

88
import styles from './index.module.css';
99

10+
type SidebarItemType = Omit<
11+
ComponentProps<typeof SidebarItem>,
12+
'as' | 'pathname'
13+
> & {
14+
items?: Array<SidebarItemType>;
15+
};
16+
1017
type SidebarGroupProps = {
1118
groupName: FormattedMessage;
12-
items: Array<Omit<ComponentProps<typeof SidebarItem>, 'as' | 'pathname'>>;
19+
items: Array<SidebarItemType>;
1320
as?: LinkLike;
1421
pathname?: string;
15-
className: string;
22+
className?: string;
23+
};
24+
25+
const hasActivePath = (
26+
items: Array<SidebarItemType>,
27+
pathname?: string
28+
): boolean => {
29+
return items.some(
30+
item =>
31+
item.link === pathname ||
32+
(item.items && hasActivePath(item.items, pathname))
33+
);
34+
};
35+
36+
const renderItems = (
37+
items: Array<SidebarItemType>,
38+
props: { as?: LinkLike },
39+
pathname?: string
40+
) => {
41+
return items.map(({ label, link, items: subItems }) => {
42+
if (subItems && subItems.length > 0) {
43+
const isOpen = hasActivePath(subItems, pathname);
44+
return (
45+
<details
46+
key={label as string}
47+
className={styles.subGroup}
48+
open={isOpen || undefined}
49+
>
50+
<summary className={styles.summary}>{label}</summary>
51+
<ul className={styles.subItemList}>
52+
{renderItems(subItems, props, pathname)}
53+
</ul>
54+
</details>
55+
);
56+
}
57+
return (
58+
<SidebarItem
59+
key={link}
60+
label={label}
61+
link={link}
62+
pathname={pathname}
63+
{...props}
64+
/>
65+
);
66+
});
1667
};
1768

1869
const SidebarGroup: FC<SidebarGroupProps> = ({
1970
groupName,
2071
items,
2172
className,
73+
pathname,
2274
...props
2375
}) => (
2476
<section className={classNames(styles.group, className)}>
2577
<label className={styles.groupName}>{groupName}</label>
26-
<ul className={styles.itemList}>
27-
{items.map(({ label, link }) => (
28-
<SidebarItem key={link} label={label} link={link} {...props} />
29-
))}
30-
</ul>
78+
<ul className={styles.itemList}>{renderItems(items, props, pathname)}</ul>
3179
</section>
3280
);
3381

packages/ui-components/src/Containers/Sidebar/index.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,25 @@ import { forwardRef } from 'react';
33
import WithNoScriptSelect from '#ui/Common/Select/NoScriptSelect';
44
import SidebarGroup from '#ui/Containers/Sidebar/SidebarGroup';
55

6-
import type { LinkLike } from '#ui/types';
6+
import type { FormattedMessage, LinkLike } from '#ui/types';
77
import type { ComponentProps, PropsWithChildren } from 'react';
88

99
import styles from './index.module.css';
1010

11+
type SidebarItemType = {
12+
label: FormattedMessage;
13+
link: string;
14+
items?: Array<SidebarItemType>;
15+
};
16+
17+
const flattenItems = (
18+
items: Array<SidebarItemType>
19+
): Array<SidebarItemType> => {
20+
return items.flatMap((item: SidebarItemType) =>
21+
item.items && item.items.length ? flattenItems(item.items) : [item]
22+
);
23+
};
24+
1125
type SidebarProps = {
1226
groups: Array<
1327
Pick<ComponentProps<typeof SidebarGroup>, 'items' | 'groupName'>
@@ -23,7 +37,9 @@ const SideBar = forwardRef<HTMLElement, PropsWithChildren<SidebarProps>>(
2337
({ groups, pathname, title, onSelect, as, children, placeholder }, ref) => {
2438
const selectItems = groups.map(({ items, groupName }) => ({
2539
label: groupName,
26-
items: items.map(({ label, link }) => ({ value: link, label })),
40+
items: flattenItems(items as Array<SidebarItemType>).map(
41+
({ label, link }) => ({ value: link, label })
42+
),
2743
}));
2844

2945
const currentItem = selectItems

0 commit comments

Comments
 (0)