mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-30 09:27:04 +02:00
fix(v2): fix FOUC in doc sidebar and various improvements (#2867)
* bug(v2): fix active sidebar item detection logic (https://github.com/facebook/docusaurus/pull/2682#issuecomment-636631225) * fix sidebar category collapsed normalization to make sure we always have a boolean after normalization * fix sidebarCollapsible option
This commit is contained in:
parent
b8de9c6ded
commit
6797af660f
7 changed files with 131 additions and 137 deletions
|
@ -9,30 +9,25 @@ module.exports = {
|
|||
docs: [
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: true,
|
||||
label: 'level 1',
|
||||
items: [
|
||||
'a',
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: true,
|
||||
label: 'level 2',
|
||||
items: [
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: true,
|
||||
label: 'level 3',
|
||||
items: [
|
||||
'c',
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: true,
|
||||
label: 'level 4',
|
||||
items: [
|
||||
'd',
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: true,
|
||||
label: 'deeper more more',
|
||||
items: ['e'],
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@ Object {
|
|||
"collapsed": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"collapsed": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"href": "/docs/foo/bar",
|
||||
|
|
|
@ -157,6 +157,7 @@ exports[`loadSidebars sidebars with first level not a category 1`] = `
|
|||
Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
"collapsed": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "greeting",
|
||||
|
|
|
@ -7,6 +7,7 @@ Object {
|
|||
"collapsed": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"collapsed": true,
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "version-1.0.0/foo/bar",
|
||||
|
|
|
@ -25,6 +25,9 @@ function isCategoryShorthand(
|
|||
return typeof item !== 'string' && !item.type;
|
||||
}
|
||||
|
||||
// categories are collapsed by default, unless user set collapsed = false
|
||||
const defaultCategoryCollapsedValue = true;
|
||||
|
||||
/**
|
||||
* Convert {category1: [item1,item2]} shorthand syntax to long-form syntax
|
||||
*/
|
||||
|
@ -33,7 +36,7 @@ function normalizeCategoryShorthand(
|
|||
): SidebarItemCategoryRaw[] {
|
||||
return Object.entries(sidebar).map(([label, items]) => ({
|
||||
type: 'category',
|
||||
collapsed: true,
|
||||
collapsed: defaultCategoryCollapsedValue,
|
||||
label,
|
||||
items,
|
||||
}));
|
||||
|
@ -118,7 +121,13 @@ function normalizeItem(item: SidebarItemRaw): SidebarItem[] {
|
|||
switch (item.type) {
|
||||
case 'category':
|
||||
assertIsCategory(item);
|
||||
return [{...item, items: flatMap(item.items, normalizeItem)}];
|
||||
return [
|
||||
{
|
||||
collapsed: defaultCategoryCollapsedValue,
|
||||
...item,
|
||||
items: flatMap(item.items, normalizeItem),
|
||||
},
|
||||
];
|
||||
case 'link':
|
||||
assertIsLink(item);
|
||||
return [item];
|
||||
|
|
|
@ -42,7 +42,7 @@ export interface SidebarItemCategory {
|
|||
type: 'category';
|
||||
label: string;
|
||||
items: SidebarItem[];
|
||||
collapsed?: boolean;
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
export interface SidebarItemCategoryRaw {
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {useState, useCallback} from 'react';
|
||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||
import React, {useState, useCallback, useEffect, useRef} from 'react';
|
||||
import classnames from 'classnames';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import useAnnouncementBarContext from '@theme/hooks/useAnnouncementBarContext';
|
||||
|
@ -20,146 +19,140 @@ import styles from './styles.module.css';
|
|||
|
||||
const MOBILE_TOGGLE_SIZE = 24;
|
||||
|
||||
function DocSidebarItem({
|
||||
function usePrevious(value) {
|
||||
const ref = useRef(value);
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
const isActiveSidebarItem = (item, activePath) => {
|
||||
if (item.type === 'link') {
|
||||
return item.href === activePath;
|
||||
}
|
||||
if (item.type === 'category') {
|
||||
return item.items.some((subItem) =>
|
||||
isActiveSidebarItem(subItem, activePath),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
function DocSidebarItemCategory({
|
||||
item,
|
||||
onItemClick,
|
||||
collapsible,
|
||||
activePath,
|
||||
...props
|
||||
}) {
|
||||
const {items, href, label, type} = item;
|
||||
const [collapsed, setCollapsed] = useState(item.collapsed);
|
||||
const [prevCollapsedProp, setPreviousCollapsedProp] = useState(null);
|
||||
const {items, label} = item;
|
||||
|
||||
// If the collapsing state from props changed, probably a navigation event
|
||||
// occurred. Overwrite the component's collapsed state with the props'
|
||||
// collapsed value.
|
||||
if (item.collapsed !== prevCollapsedProp) {
|
||||
setPreviousCollapsedProp(item.collapsed);
|
||||
setCollapsed(item.collapsed);
|
||||
}
|
||||
const isActive = isActiveSidebarItem(item, activePath);
|
||||
const wasActive = usePrevious(isActive);
|
||||
|
||||
const handleItemClick = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
e.target.blur();
|
||||
setCollapsed((state) => !state);
|
||||
// active categories are always initialized as expanded
|
||||
// the default (item.collapsed) is only used for non-active categories
|
||||
const [collapsed, setCollapsed] = useState(() => {
|
||||
if (!collapsible) {
|
||||
return false;
|
||||
}
|
||||
return isActive ? false : item.collapsed;
|
||||
});
|
||||
|
||||
// Make sure we have access to the window
|
||||
const activePageRelativeUrl = ExecutionEnvironment.canUseDOM
|
||||
? window.location.pathname + window.location.search
|
||||
: null;
|
||||
|
||||
// We need to know if the category item
|
||||
// is the parent of the active page
|
||||
// If it is, this returns true and make sure to highlight this category
|
||||
const isCategoryOfActivePage = (_items, _activePageRelativeUrl) => {
|
||||
// Make sure we have items
|
||||
if (typeof _items !== 'undefined') {
|
||||
return _items.some((categoryItem) => {
|
||||
// Grab the category item's href
|
||||
const childHref = categoryItem.href;
|
||||
// Compare it to the current active page
|
||||
return _activePageRelativeUrl === childHref;
|
||||
});
|
||||
// If we navigate to a category, it should automatically expand itself
|
||||
useEffect(() => {
|
||||
const justBecameActive = isActive && !wasActive;
|
||||
if (justBecameActive && collapsed) {
|
||||
setCollapsed(false);
|
||||
}
|
||||
}, [isActive, wasActive, collapsed]);
|
||||
|
||||
return false;
|
||||
};
|
||||
const handleItemClick = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.target.blur();
|
||||
setCollapsed((state) => !state);
|
||||
},
|
||||
[setCollapsed],
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case 'category':
|
||||
return (
|
||||
items.length > 0 && (
|
||||
<li
|
||||
className={classnames('menu__list-item', {
|
||||
'menu__list-item--collapsed': collapsed,
|
||||
})}
|
||||
key={label}>
|
||||
<a
|
||||
className={classnames('menu__link', {
|
||||
'menu__link--sublist': collapsible,
|
||||
'menu__link--active':
|
||||
collapsible &&
|
||||
!item.collapsed &&
|
||||
isCategoryOfActivePage(items, activePageRelativeUrl),
|
||||
})}
|
||||
href="#!"
|
||||
onClick={collapsible ? handleItemClick : undefined}
|
||||
{...props}>
|
||||
{label}
|
||||
</a>
|
||||
<ul className="menu__list">
|
||||
{items.map((childItem) => (
|
||||
<DocSidebarItem
|
||||
tabIndex={collapsed ? '-1' : '0'}
|
||||
key={childItem.label}
|
||||
item={childItem}
|
||||
onItemClick={onItemClick}
|
||||
collapsible={collapsible}
|
||||
activePath={activePath}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
|
||||
case 'link':
|
||||
default:
|
||||
return (
|
||||
<li className="menu__list-item" key={label}>
|
||||
<Link
|
||||
className={classnames('menu__link', {
|
||||
'menu__link--active': href === activePath,
|
||||
})}
|
||||
to={href}
|
||||
{...(isInternalUrl(href)
|
||||
? {
|
||||
isNavLink: true,
|
||||
exact: true,
|
||||
onClick: onItemClick,
|
||||
}
|
||||
: {
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener',
|
||||
})}
|
||||
{...props}>
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classnames('menu__list-item', {
|
||||
'menu__list-item--collapsed': collapsed,
|
||||
})}
|
||||
key={label}>
|
||||
<a
|
||||
className={classnames('menu__link', {
|
||||
'menu__link--sublist': collapsible,
|
||||
'menu__link--active': collapsible && isActive,
|
||||
})}
|
||||
href="#!"
|
||||
onClick={collapsible ? handleItemClick : undefined}
|
||||
{...props}>
|
||||
{label}
|
||||
</a>
|
||||
<ul className="menu__list">
|
||||
{items.map((childItem) => (
|
||||
<DocSidebarItem
|
||||
tabIndex={collapsed ? '-1' : '0'}
|
||||
key={childItem.label}
|
||||
item={childItem}
|
||||
onItemClick={onItemClick}
|
||||
collapsible={collapsible}
|
||||
activePath={activePath}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate the category collapsing state when a page navigation occurs.
|
||||
// We want to automatically expand the categories which contains the current page.
|
||||
function mutateSidebarCollapsingState(item, path) {
|
||||
const {items, href, type} = item;
|
||||
switch (type) {
|
||||
case 'category': {
|
||||
const anyChildItemsActive =
|
||||
items
|
||||
.map((childItem) => mutateSidebarCollapsingState(childItem, path))
|
||||
.filter((val) => val).length > 0;
|
||||
|
||||
// Check if the user wants the category to be expanded by default
|
||||
const shouldExpand = item.collapsed === false;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.collapsed = !anyChildItemsActive;
|
||||
|
||||
if (shouldExpand) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.collapsed = false;
|
||||
}
|
||||
|
||||
return anyChildItemsActive;
|
||||
}
|
||||
function DocSidebarItemLink({
|
||||
item,
|
||||
onItemClick,
|
||||
activePath,
|
||||
collapsible,
|
||||
...props
|
||||
}) {
|
||||
const {href, label} = item;
|
||||
const isActive = isActiveSidebarItem(item, activePath);
|
||||
return (
|
||||
<li className="menu__list-item" key={label}>
|
||||
<Link
|
||||
className={classnames('menu__link', {
|
||||
'menu__link--active': isActive,
|
||||
})}
|
||||
to={href}
|
||||
{...(isInternalUrl(href)
|
||||
? {
|
||||
isNavLink: true,
|
||||
exact: true,
|
||||
onClick: onItemClick,
|
||||
}
|
||||
: {
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener',
|
||||
})}
|
||||
{...props}>
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function DocSidebarItem(props) {
|
||||
switch (props.item.type) {
|
||||
case 'category':
|
||||
return <DocSidebarItemCategory {...props} />;
|
||||
case 'link':
|
||||
default:
|
||||
return href === path;
|
||||
return <DocSidebarItemLink {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,12 +189,6 @@ function DocSidebar(props) {
|
|||
);
|
||||
}
|
||||
|
||||
if (sidebarCollapsible) {
|
||||
sidebarData.forEach((sidebarItem) =>
|
||||
mutateSidebarCollapsingState(sidebarItem, path),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(styles.sidebar, {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue