Skip to content

Commit 26d6941

Browse files
authored
Merge pull request #183 from GBSL-Informatik/fix/revert-category-collapse-behavior
Fix: Revert category collapse behavior.
2 parents b6dad24 + be57521 commit 26d6941

File tree

1 file changed

+215
-0
lines changed

1 file changed

+215
-0
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import React, { type ComponentProps, type ReactNode, useEffect, useMemo } from 'react';
2+
import clsx from 'clsx';
3+
import {
4+
ThemeClassNames,
5+
useThemeConfig,
6+
usePrevious,
7+
Collapsible,
8+
useCollapsible
9+
} from '@docusaurus/theme-common';
10+
import { isSamePath } from '@docusaurus/theme-common/internal';
11+
import {
12+
isActiveSidebarItem,
13+
findFirstSidebarItemLink,
14+
useDocSidebarItemsExpandedState
15+
} from '@docusaurus/plugin-content-docs/client';
16+
import Link from '@docusaurus/Link';
17+
import { translate } from '@docusaurus/Translate';
18+
import useIsBrowser from '@docusaurus/useIsBrowser';
19+
import DocSidebarItems from '@theme/DocSidebarItems';
20+
import type { Props } from '@theme/DocSidebarItem/Category';
21+
22+
// If we navigate to a category and it becomes active, it should automatically
23+
// expand itself
24+
function useAutoExpandActiveCategory({
25+
isActive,
26+
collapsed,
27+
updateCollapsed
28+
}: {
29+
isActive: boolean;
30+
collapsed: boolean;
31+
updateCollapsed: (b: boolean) => void;
32+
}) {
33+
const wasActive = usePrevious(isActive);
34+
useEffect(() => {
35+
const justBecameActive = isActive && !wasActive;
36+
if (justBecameActive && collapsed) {
37+
updateCollapsed(false);
38+
}
39+
}, [isActive, wasActive, collapsed, updateCollapsed]);
40+
}
41+
42+
/**
43+
* When a collapsible category has no link, we still link it to its first child
44+
* during SSR as a temporary fallback. This allows to be able to navigate inside
45+
* the category even when JS fails to load, is delayed or simply disabled
46+
* React hydration becomes an optional progressive enhancement
47+
* see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188
48+
* see https://github.com/facebook/docusaurus/issues/3030
49+
*/
50+
function useCategoryHrefWithSSRFallback(item: Props['item']): string | undefined {
51+
const isBrowser = useIsBrowser();
52+
return useMemo(() => {
53+
if (item.href && !item.linkUnlisted) {
54+
return item.href;
55+
}
56+
// In these cases, it's not necessary to render a fallback
57+
// We skip the "findFirstCategoryLink" computation
58+
if (isBrowser || !item.collapsible) {
59+
return undefined;
60+
}
61+
return findFirstSidebarItemLink(item);
62+
}, [item, isBrowser]);
63+
}
64+
65+
function CollapseButton({
66+
collapsed,
67+
categoryLabel,
68+
onClick
69+
}: {
70+
collapsed: boolean;
71+
categoryLabel: string;
72+
onClick: ComponentProps<'button'>['onClick'];
73+
}) {
74+
return (
75+
<button
76+
aria-label={
77+
collapsed
78+
? translate(
79+
{
80+
id: 'theme.DocSidebarItem.expandCategoryAriaLabel',
81+
message: "Expand sidebar category '{label}'",
82+
description: 'The ARIA label to expand the sidebar category'
83+
},
84+
{ label: categoryLabel }
85+
)
86+
: translate(
87+
{
88+
id: 'theme.DocSidebarItem.collapseCategoryAriaLabel',
89+
message: "Collapse sidebar category '{label}'",
90+
description: 'The ARIA label to collapse the sidebar category'
91+
},
92+
{ label: categoryLabel }
93+
)
94+
}
95+
aria-expanded={!collapsed}
96+
type="button"
97+
className="clean-btn menu__caret"
98+
onClick={onClick}
99+
/>
100+
);
101+
}
102+
103+
export default function DocSidebarItemCategory({
104+
item,
105+
onItemClick,
106+
activePath,
107+
level,
108+
index,
109+
...props
110+
}: Props): ReactNode {
111+
const { items, label, collapsible, className, href } = item;
112+
const {
113+
docs: {
114+
sidebar: { autoCollapseCategories }
115+
}
116+
} = useThemeConfig();
117+
const hrefWithSSRFallback = useCategoryHrefWithSSRFallback(item);
118+
119+
const isActive = isActiveSidebarItem(item, activePath);
120+
const isCurrentPage = isSamePath(href, activePath);
121+
122+
const { collapsed, setCollapsed } = useCollapsible({
123+
// Active categories are always initialized as expanded. The default
124+
// (`item.collapsed`) is only used for non-active categories.
125+
initialState: () => {
126+
if (!collapsible) {
127+
return false;
128+
}
129+
return isActive ? false : item.collapsed;
130+
}
131+
});
132+
133+
const { expandedItem, setExpandedItem } = useDocSidebarItemsExpandedState();
134+
// Use this instead of `setCollapsed`, because it is also reactive
135+
const updateCollapsed = (toCollapsed: boolean = !collapsed) => {
136+
setExpandedItem(toCollapsed ? null : index);
137+
setCollapsed(toCollapsed);
138+
};
139+
useAutoExpandActiveCategory({ isActive, collapsed, updateCollapsed });
140+
useEffect(() => {
141+
if (collapsible && expandedItem != null && expandedItem !== index && autoCollapseCategories) {
142+
setCollapsed(true);
143+
}
144+
}, [collapsible, expandedItem, index, setCollapsed, autoCollapseCategories]);
145+
146+
return (
147+
<li
148+
className={clsx(
149+
ThemeClassNames.docs.docSidebarItemCategory,
150+
ThemeClassNames.docs.docSidebarItemCategoryLevel(level),
151+
'menu__list-item',
152+
{
153+
'menu__list-item--collapsed': collapsed
154+
},
155+
className
156+
)}
157+
>
158+
<div
159+
className={clsx('menu__list-item-collapsible', {
160+
'menu__list-item-collapsible--active': isCurrentPage
161+
})}
162+
>
163+
<Link
164+
className={clsx('menu__link', {
165+
'menu__link--sublist': collapsible,
166+
'menu__link--sublist-caret': !href && collapsible,
167+
'menu__link--active': isActive
168+
})}
169+
onClick={
170+
collapsible
171+
? (e) => {
172+
onItemClick?.(item);
173+
if (href) {
174+
updateCollapsed(false);
175+
} else {
176+
e.preventDefault();
177+
updateCollapsed();
178+
}
179+
}
180+
: () => {
181+
onItemClick?.(item);
182+
}
183+
}
184+
aria-current={isCurrentPage ? 'page' : undefined}
185+
role={collapsible && !href ? 'button' : undefined}
186+
aria-expanded={collapsible && !href ? !collapsed : undefined}
187+
href={collapsible ? (hrefWithSSRFallback ?? '#') : hrefWithSSRFallback}
188+
{...props}
189+
>
190+
{label}
191+
</Link>
192+
{href && collapsible && (
193+
<CollapseButton
194+
collapsed={collapsed}
195+
categoryLabel={label}
196+
onClick={(e) => {
197+
e.preventDefault();
198+
updateCollapsed();
199+
}}
200+
/>
201+
)}
202+
</div>
203+
204+
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
205+
<DocSidebarItems
206+
items={items}
207+
tabIndex={collapsed ? -1 : 0}
208+
onItemClick={onItemClick}
209+
activePath={activePath}
210+
level={level + 1}
211+
/>
212+
</Collapsible>
213+
</li>
214+
);
215+
}

0 commit comments

Comments
 (0)