Skip to content

Commit 7d8abad

Browse files
authored
Merge pull request #184 from chiztechnology/navigation-label-resize
Implement text auto resize component for best label rendering on small device
2 parents 044cf34 + 122cb9a commit 7d8abad

File tree

4 files changed

+479
-0
lines changed

4 files changed

+479
-0
lines changed
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
/**
2+
* Custom category item used by the docs sidebar. Only a small portion of the
3+
* original file is modified: the label rendering. We truncate long labels and
4+
* add a tooltip so the UI doesn't break on mobile.
5+
*
6+
* The rest of this component is copied verbatim from the upstream theme so that
7+
* behavior remains compatible with future releases; updates can be pulled in by
8+
* re‑swizzling or diffing against the official source.
9+
*/
10+
11+
import React, {
12+
type ComponentProps,
13+
type ReactNode,
14+
useEffect,
15+
useMemo,
16+
} from 'react';
17+
import clsx from 'clsx';
18+
import {
19+
ThemeClassNames,
20+
useThemeConfig,
21+
usePrevious,
22+
Collapsible,
23+
useCollapsible,
24+
} from '@docusaurus/theme-common';
25+
import {isSamePath} from '@docusaurus/theme-common/internal';
26+
import {
27+
isActiveSidebarItem,
28+
findFirstSidebarItemLink,
29+
useDocSidebarItemsExpandedState,
30+
useVisibleSidebarItems,
31+
} from '@docusaurus/plugin-content-docs/client';
32+
import Link from '@docusaurus/Link';
33+
import isInternalUrl from '@docusaurus/isInternalUrl';
34+
import {translate} from '@docusaurus/Translate';
35+
import useIsBrowser from '@docusaurus/useIsBrowser';
36+
import DocSidebarItems from '@theme/DocSidebarItems';
37+
import DocSidebarItemLink from '@theme/DocSidebarItem/Link';
38+
import type {Props} from '@theme/DocSidebarItem/Category';
39+
40+
import type {
41+
PropSidebarItemCategory,
42+
PropSidebarItemLink,
43+
} from '@docusaurus/plugin-content-docs';
44+
import styles from './styles.module.css';
45+
46+
// same helper used in Link component
47+
const MAX_LABEL_LENGTH = 25;
48+
function truncateText(label: string) {
49+
if (label.length <= MAX_LABEL_LENGTH) {
50+
return label;
51+
}
52+
return label.substring(0, MAX_LABEL_LENGTH - 3) + '...';
53+
}
54+
55+
function CategoryLinkLabel({label}: {label: string}) {
56+
const display = truncateText(label);
57+
return (
58+
<span title={label} className={styles.categoryLinkLabel}>
59+
{display}
60+
</span>
61+
);
62+
}
63+
64+
// rest of file unchanged, copied from upstream
65+
66+
// If we navigate to a category and it becomes active, it should automatically
67+
// expand itself
68+
function useAutoExpandActiveCategory({
69+
isActive,
70+
collapsed,
71+
updateCollapsed,
72+
activePath,
73+
}: {
74+
isActive: boolean;
75+
collapsed: boolean;
76+
updateCollapsed: (b: boolean) => void;
77+
activePath: string;
78+
}) {
79+
const wasActive = usePrevious(isActive);
80+
const previousActivePath = usePrevious(activePath);
81+
useEffect(() => {
82+
const justBecameActive = isActive && !wasActive;
83+
const stillActiveButPathChanged =
84+
isActive && wasActive && activePath !== previousActivePath;
85+
if ((justBecameActive || stillActiveButPathChanged) && collapsed) {
86+
updateCollapsed(false);
87+
}
88+
}, [
89+
isActive,
90+
wasActive,
91+
collapsed,
92+
updateCollapsed,
93+
activePath,
94+
previousActivePath,
95+
]);
96+
}
97+
98+
/**
99+
* When a collapsible category has no link, we still link it to its first child
100+
* during SSR as a temporary fallback. This allows to be able to navigate inside
101+
* the category even when JS fails to load, is delayed or simply disabled
102+
* React hydration becomes an optional progressive enhancement
103+
* see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543
104+
* see https://github.com/facebook/docusaurus/issues/3030
105+
*/
106+
function useCategoryHrefWithSSRFallback(
107+
item: Props['item'],
108+
): string | undefined {
109+
const isBrowser = useIsBrowser();
110+
return useMemo(() => {
111+
if (item.href && !item.linkUnlisted) {
112+
return item.href;
113+
}
114+
// In these cases, it's not necessary to render a fallback
115+
// We skip the "findFirstCategoryLink" computation
116+
if (isBrowser || !item.collapsible) {
117+
return undefined;
118+
}
119+
return findFirstSidebarItemLink(item);
120+
}, [item, isBrowser]);
121+
}
122+
123+
function CollapseButton({
124+
collapsed,
125+
categoryLabel,
126+
onClick,
127+
}: {
128+
collapsed: boolean;
129+
categoryLabel: string;
130+
onClick: ComponentProps<'button'>['onClick'];
131+
}) {
132+
return (
133+
<button
134+
aria-label={
135+
collapsed
136+
? translate(
137+
{
138+
id: 'theme.DocSidebarItem.expandCategoryAriaLabel',
139+
message: "Expand sidebar category '{label}'",
140+
description: 'The ARIA label to expand the sidebar category',
141+
},
142+
{label: categoryLabel},
143+
)
144+
: translate(
145+
{
146+
id: 'theme.DocSidebarItem.collapseCategoryAriaLabel',
147+
message: "Collapse sidebar category '{label}'",
148+
description: 'The ARIA label to collapse the sidebar category',
149+
},
150+
{label: categoryLabel},
151+
)
152+
}
153+
aria-expanded={!collapsed}
154+
type="button"
155+
className="clean-btn menu__caret"
156+
onClick={onClick}
157+
>
158+
{/* SVG chevron; right when collapsed, down when expanded */}
159+
<svg
160+
className={styles.caretIcon}
161+
aria-hidden="true"
162+
viewBox="0 0 8 8"
163+
width="8"
164+
height="8">
165+
{collapsed ? (
166+
<path
167+
d="M2 1l4 3-4 3"
168+
fill="none"
169+
stroke="currentColor"
170+
strokeWidth="0.5"
171+
strokeLinecap="round"
172+
strokeLinejoin="round"
173+
/>
174+
) : (
175+
<path
176+
d="M1 2l3 4 3-4"
177+
fill="none"
178+
stroke="currentColor"
179+
strokeWidth="0.5"
180+
strokeLinecap="round"
181+
strokeLinejoin="round"
182+
/>
183+
)}
184+
</svg>
185+
</button>
186+
);
187+
}
188+
189+
export default function DocSidebarItemCategory(props: Props): ReactNode {
190+
const visibleChildren = useVisibleSidebarItems(
191+
props.item.items,
192+
props.activePath,
193+
);
194+
if (visibleChildren.length === 0) {
195+
return <DocSidebarItemCategoryEmpty {...props} />;
196+
} else {
197+
return <DocSidebarItemCategoryCollapsible {...props} />;
198+
}
199+
}
200+
201+
function isCategoryWithHref(
202+
category: PropSidebarItemCategory,
203+
): category is PropSidebarItemCategory & {href: string} {
204+
return typeof category.href === 'string';
205+
}
206+
207+
// If a category doesn't have any visible children, we render it as a link
208+
function DocSidebarItemCategoryEmpty({item, ...props}: Props): ReactNode {
209+
// If the category has no link, we don't render anything
210+
// It's not super useful to render a category you can't open nor click
211+
if (!isCategoryWithHref(item)) {
212+
return null;
213+
}
214+
// We remove props that don't make sense for a link and forward the rest
215+
const {
216+
type,
217+
collapsed,
218+
collapsible,
219+
items,
220+
linkUnlisted,
221+
...forwardableProps
222+
} = item;
223+
const linkItem: PropSidebarItemLink = {
224+
type: 'link',
225+
...forwardableProps,
226+
};
227+
return <DocSidebarItemLink item={linkItem} {...props} />;
228+
}
229+
230+
function DocSidebarItemCategoryCollapsible({
231+
item,
232+
onItemClick,
233+
activePath,
234+
level,
235+
index,
236+
...props
237+
}: Props): ReactNode {
238+
const {items, label, collapsible, className, href} = item;
239+
const {
240+
docs: {
241+
sidebar: {autoCollapseCategories},
242+
},
243+
} = useThemeConfig();
244+
const hrefWithSSRFallback = useCategoryHrefWithSSRFallback(item);
245+
const isActive = isActiveSidebarItem(item, activePath);
246+
const {
247+
collapsed,
248+
toggleCollapsed,
249+
setCollapsed: setCollapsedRaw,
250+
} = useCollapsible({
251+
initialState: () =>
252+
!item.collapsible ||
253+
(item.collapsible && item.collapsed) ||
254+
(isActive && !autoCollapseCategories),
255+
});
256+
const setCollapsed = (value: boolean) => {
257+
// categories are never collapsed when they are active (clicked on)
258+
if (!value && isActive) {
259+
return;
260+
}
261+
262+
setCollapsedRaw(value);
263+
if (onItemClick) {
264+
onItemClick(item);
265+
}
266+
};
267+
useAutoExpandActiveCategory({
268+
isActive,
269+
collapsed,
270+
updateCollapsed: setCollapsed,
271+
activePath,
272+
});
273+
274+
// expanded state across all sidebar items (used for auto-collapsing)
275+
const {expandedItem, setExpandedItem} = useDocSidebarItemsExpandedState();
276+
// Use this instead of `setCollapsed`, because it is also reactive
277+
const updateCollapsed = (toCollapsed: boolean = !collapsed) => {
278+
setExpandedItem(toCollapsed ? null : index);
279+
setCollapsed(toCollapsed);
280+
};
281+
282+
const itemsProps = {
283+
...props,
284+
level: level + 1,
285+
activePath,
286+
onItemClick,
287+
index,
288+
};
289+
return (
290+
<li
291+
className={clsx(
292+
ThemeClassNames.docs.docSidebarItemCategory,
293+
ThemeClassNames.docs.docSidebarItemCategoryLevel(level),
294+
'menu__list-item',
295+
className,
296+
)}
297+
key={label}>
298+
<div
299+
className={clsx(styles.categoryLink, {
300+
[styles.categoryLinkWithHref]: hrefWithSSRFallback,
301+
})}>
302+
{hrefWithSSRFallback ? (
303+
<Link
304+
className={clsx('menu__link', {
305+
'menu__link--active': isActive,
306+
'menu__link--sublist': true,
307+
})}
308+
to={hrefWithSSRFallback}
309+
{...(href && isInternalUrl(href) && {
310+
onClick: onItemClick ? () => onItemClick(item) : undefined,
311+
})}>
312+
<CategoryLinkLabel label={label} />
313+
</Link>
314+
) : (
315+
<span
316+
className={clsx('menu__link', 'menu__link--sublist', {
317+
'menu__link--active': isActive,
318+
})}>
319+
<CategoryLinkLabel label={label} />
320+
</span>
321+
)}
322+
{collapsible && (
323+
<CollapseButton
324+
collapsed={collapsed}
325+
categoryLabel={label}
326+
onClick={toggleCollapsed}
327+
/>
328+
)}
329+
</div>
330+
{collapsible ? (
331+
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
332+
<DocSidebarItems items={items} {...itemsProps} />
333+
</Collapsible>
334+
) : (
335+
<DocSidebarItems items={items} {...itemsProps} />
336+
)}
337+
</li>
338+
);
339+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Styles for the swizzled category item. We're reducing the label to a
3+
* single line with ellipsis so that long category names don't force wrapped
4+
* items and break the sidebar on narrow viewports.
5+
*/
6+
7+
.categoryLink {
8+
overflow: hidden;
9+
display: flex;
10+
align-items: center;
11+
}
12+
13+
/* never let the toggle button shrink to zero width */
14+
.categoryLink > button {
15+
flex-shrink: 0;
16+
}
17+
18+
.categoryLinkLabel {
19+
flex: 1 1 auto;
20+
overflow: hidden;
21+
white-space: nowrap;
22+
text-overflow: ellipsis;
23+
}
24+
25+
/* preserve existing behaviour for submenu caret */
26+
:global(.menu__link--sublist-caret)::after {
27+
margin-left: var(--ifm-menu-link-padding-vertical);
28+
}
29+
30+
/* explicit icon inside collapse button */
31+
.caretIcon {
32+
display: inline-flex;
33+
align-items: center;
34+
justify-content: center;
35+
width: 1rem;
36+
height: 1rem;
37+
color: var(--ifm-color-menu-link); /* match normal menu text */
38+
flex-shrink: 0;
39+
}

0 commit comments

Comments
 (0)