Skip to content

Commit 620b605

Browse files
Venkat-EntropikVenkat
andauthored
feat(client): add nested sections support with auto-expansion in sidebar (#8806)
* feat(client): add nested sections support with auto-expansion in sidebar * fix: resolve sidebar key and DOM nesting issues * fix: update stories for nested sidebar structure * fix: expand sidebar group when parent route is active * fix: restrict subGroup open style to direct summary children --------- Co-authored-by: Venkat <venkat@Venkats-MacBook-Pro.local>
1 parent 97e28c5 commit 620b605

5 files changed

Lines changed: 165 additions & 12 deletions

File tree

apps/site/components/withSidebar.tsx

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

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

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

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

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.stories.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,27 @@ export const EmptyGroup: Story = {
3333
},
3434
};
3535

36+
export const NestedGroup: Story = {
37+
args: {
38+
groupName: 'Nested Group',
39+
pathname: '/nested/folder-b/leaf-2',
40+
items: [
41+
{ label: 'Flat Item', link: '/nested/flat' },
42+
{
43+
label: 'Folder A',
44+
link: '/nested/folder-a',
45+
items: [{ label: 'Leaf A.1', link: '/nested/folder-a/leaf-1' }],
46+
},
47+
{
48+
label: 'Folder B',
49+
link: '/nested/folder-b',
50+
items: [
51+
{ label: 'Leaf B.1', link: '/nested/folder-b/leaf-1' },
52+
{ label: 'Leaf B.2 (Active)', link: '/nested/folder-b/leaf-2' },
53+
],
54+
},
55+
],
56+
},
57+
};
58+
3659
export default { component: SidebarGroup } as Meta;

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

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,73 @@ 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 = link === pathname || hasActivePath(subItems, pathname);
44+
return (
45+
<li key={link}>
46+
<details className={styles.subGroup} open={isOpen}>
47+
<summary className={styles.summary}>{label}</summary>
48+
<ul className={styles.subItemList}>
49+
{renderItems(subItems, props, pathname)}
50+
</ul>
51+
</details>
52+
</li>
53+
);
54+
}
55+
return (
56+
<SidebarItem
57+
key={link}
58+
label={label}
59+
link={link}
60+
pathname={pathname}
61+
{...props}
62+
/>
63+
);
64+
});
1665
};
1766

1867
const SidebarGroup: FC<SidebarGroupProps> = ({
1968
groupName,
2069
items,
2170
className,
71+
pathname,
2272
...props
2373
}) => (
2474
<section className={classNames(styles.group, className)}>
2575
<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>
76+
<ul className={styles.itemList}>{renderItems(items, props, pathname)}</ul>
3177
</section>
3278
);
3379

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
import WithNoScriptSelect from '#ui/Common/Select/NoScriptSelect';
22
import SidebarGroup from '#ui/Containers/Sidebar/SidebarGroup';
33

4-
import type { LinkLike } from '#ui/types';
4+
import type { FormattedMessage, LinkLike } from '#ui/types';
55
import type { ComponentProps, FC, PropsWithChildren, RefObject } from 'react';
66

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

9+
type SidebarItemType = {
10+
label: FormattedMessage;
11+
link: string;
12+
items?: Array<SidebarItemType>;
13+
};
14+
15+
const flattenItems = (
16+
items: Array<SidebarItemType>
17+
): Array<SidebarItemType> => {
18+
return items.flatMap((item: SidebarItemType) =>
19+
item.items && item.items.length ? flattenItems(item.items) : [item]
20+
);
21+
};
22+
923
type SidebarProps = {
1024
groups: Array<
1125
Pick<ComponentProps<typeof SidebarGroup>, 'items' | 'groupName'>
@@ -30,7 +44,9 @@ const SideBar: FC<PropsWithChildren<SidebarProps>> = ({
3044
}) => {
3145
const selectItems = groups.map(({ items, groupName }) => ({
3246
label: groupName,
33-
items: items.map(({ label, link }) => ({ value: link, label })),
47+
items: flattenItems(items as Array<SidebarItemType>).map(
48+
({ label, link }) => ({ value: link, label })
49+
),
3450
}));
3551

3652
const currentItem = selectItems

0 commit comments

Comments
 (0)