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: [
|
docs: [
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
collapsed: true,
|
|
||||||
label: 'level 1',
|
label: 'level 1',
|
||||||
items: [
|
items: [
|
||||||
'a',
|
'a',
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
collapsed: true,
|
|
||||||
label: 'level 2',
|
label: 'level 2',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
collapsed: true,
|
|
||||||
label: 'level 3',
|
label: 'level 3',
|
||||||
items: [
|
items: [
|
||||||
'c',
|
'c',
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
collapsed: true,
|
|
||||||
label: 'level 4',
|
label: 'level 4',
|
||||||
items: [
|
items: [
|
||||||
'd',
|
'd',
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
collapsed: true,
|
|
||||||
label: 'deeper more more',
|
label: 'deeper more more',
|
||||||
items: ['e'],
|
items: ['e'],
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,7 @@ Object {
|
||||||
"collapsed": true,
|
"collapsed": true,
|
||||||
"items": Array [
|
"items": Array [
|
||||||
Object {
|
Object {
|
||||||
|
"collapsed": true,
|
||||||
"items": Array [
|
"items": Array [
|
||||||
Object {
|
Object {
|
||||||
"href": "/docs/foo/bar",
|
"href": "/docs/foo/bar",
|
||||||
|
|
|
@ -157,6 +157,7 @@ exports[`loadSidebars sidebars with first level not a category 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"docs": Array [
|
"docs": Array [
|
||||||
Object {
|
Object {
|
||||||
|
"collapsed": true,
|
||||||
"items": Array [
|
"items": Array [
|
||||||
Object {
|
Object {
|
||||||
"id": "greeting",
|
"id": "greeting",
|
||||||
|
|
|
@ -7,6 +7,7 @@ Object {
|
||||||
"collapsed": true,
|
"collapsed": true,
|
||||||
"items": Array [
|
"items": Array [
|
||||||
Object {
|
Object {
|
||||||
|
"collapsed": true,
|
||||||
"items": Array [
|
"items": Array [
|
||||||
Object {
|
Object {
|
||||||
"id": "version-1.0.0/foo/bar",
|
"id": "version-1.0.0/foo/bar",
|
||||||
|
|
|
@ -25,6 +25,9 @@ function isCategoryShorthand(
|
||||||
return typeof item !== 'string' && !item.type;
|
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
|
* Convert {category1: [item1,item2]} shorthand syntax to long-form syntax
|
||||||
*/
|
*/
|
||||||
|
@ -33,7 +36,7 @@ function normalizeCategoryShorthand(
|
||||||
): SidebarItemCategoryRaw[] {
|
): SidebarItemCategoryRaw[] {
|
||||||
return Object.entries(sidebar).map(([label, items]) => ({
|
return Object.entries(sidebar).map(([label, items]) => ({
|
||||||
type: 'category',
|
type: 'category',
|
||||||
collapsed: true,
|
collapsed: defaultCategoryCollapsedValue,
|
||||||
label,
|
label,
|
||||||
items,
|
items,
|
||||||
}));
|
}));
|
||||||
|
@ -118,7 +121,13 @@ function normalizeItem(item: SidebarItemRaw): SidebarItem[] {
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case 'category':
|
case 'category':
|
||||||
assertIsCategory(item);
|
assertIsCategory(item);
|
||||||
return [{...item, items: flatMap(item.items, normalizeItem)}];
|
return [
|
||||||
|
{
|
||||||
|
collapsed: defaultCategoryCollapsedValue,
|
||||||
|
...item,
|
||||||
|
items: flatMap(item.items, normalizeItem),
|
||||||
|
},
|
||||||
|
];
|
||||||
case 'link':
|
case 'link':
|
||||||
assertIsLink(item);
|
assertIsLink(item);
|
||||||
return [item];
|
return [item];
|
||||||
|
|
|
@ -42,7 +42,7 @@ export interface SidebarItemCategory {
|
||||||
type: 'category';
|
type: 'category';
|
||||||
label: string;
|
label: string;
|
||||||
items: SidebarItem[];
|
items: SidebarItem[];
|
||||||
collapsed?: boolean;
|
collapsed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarItemCategoryRaw {
|
export interface SidebarItemCategoryRaw {
|
||||||
|
|
|
@ -5,8 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useState, useCallback} from 'react';
|
import React, {useState, useCallback, useEffect, useRef} from 'react';
|
||||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
import useAnnouncementBarContext from '@theme/hooks/useAnnouncementBarContext';
|
import useAnnouncementBarContext from '@theme/hooks/useAnnouncementBarContext';
|
||||||
|
@ -20,146 +19,140 @@ import styles from './styles.module.css';
|
||||||
|
|
||||||
const MOBILE_TOGGLE_SIZE = 24;
|
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,
|
item,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
collapsible,
|
collapsible,
|
||||||
activePath,
|
activePath,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
const {items, href, label, type} = item;
|
const {items, label} = item;
|
||||||
const [collapsed, setCollapsed] = useState(item.collapsed);
|
|
||||||
const [prevCollapsedProp, setPreviousCollapsedProp] = useState(null);
|
|
||||||
|
|
||||||
// If the collapsing state from props changed, probably a navigation event
|
const isActive = isActiveSidebarItem(item, activePath);
|
||||||
// occurred. Overwrite the component's collapsed state with the props'
|
const wasActive = usePrevious(isActive);
|
||||||
// collapsed value.
|
|
||||||
if (item.collapsed !== prevCollapsedProp) {
|
|
||||||
setPreviousCollapsedProp(item.collapsed);
|
|
||||||
setCollapsed(item.collapsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleItemClick = useCallback((e) => {
|
// active categories are always initialized as expanded
|
||||||
e.preventDefault();
|
// the default (item.collapsed) is only used for non-active categories
|
||||||
e.target.blur();
|
const [collapsed, setCollapsed] = useState(() => {
|
||||||
setCollapsed((state) => !state);
|
if (!collapsible) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isActive ? false : item.collapsed;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make sure we have access to the window
|
// If we navigate to a category, it should automatically expand itself
|
||||||
const activePageRelativeUrl = ExecutionEnvironment.canUseDOM
|
useEffect(() => {
|
||||||
? window.location.pathname + window.location.search
|
const justBecameActive = isActive && !wasActive;
|
||||||
: null;
|
if (justBecameActive && collapsed) {
|
||||||
|
setCollapsed(false);
|
||||||
// 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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}, [isActive, wasActive, collapsed]);
|
||||||
|
|
||||||
return false;
|
const handleItemClick = useCallback(
|
||||||
};
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.target.blur();
|
||||||
|
setCollapsed((state) => !state);
|
||||||
|
},
|
||||||
|
[setCollapsed],
|
||||||
|
);
|
||||||
|
|
||||||
switch (type) {
|
if (items.length === 0) {
|
||||||
case 'category':
|
return null;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
function DocSidebarItemLink({
|
||||||
// We want to automatically expand the categories which contains the current page.
|
item,
|
||||||
function mutateSidebarCollapsingState(item, path) {
|
onItemClick,
|
||||||
const {items, href, type} = item;
|
activePath,
|
||||||
switch (type) {
|
collapsible,
|
||||||
case 'category': {
|
...props
|
||||||
const anyChildItemsActive =
|
}) {
|
||||||
items
|
const {href, label} = item;
|
||||||
.map((childItem) => mutateSidebarCollapsingState(childItem, path))
|
const isActive = isActiveSidebarItem(item, activePath);
|
||||||
.filter((val) => val).length > 0;
|
return (
|
||||||
|
<li className="menu__list-item" key={label}>
|
||||||
// Check if the user wants the category to be expanded by default
|
<Link
|
||||||
const shouldExpand = item.collapsed === false;
|
className={classnames('menu__link', {
|
||||||
|
'menu__link--active': isActive,
|
||||||
// eslint-disable-next-line no-param-reassign
|
})}
|
||||||
item.collapsed = !anyChildItemsActive;
|
to={href}
|
||||||
|
{...(isInternalUrl(href)
|
||||||
if (shouldExpand) {
|
? {
|
||||||
// eslint-disable-next-line no-param-reassign
|
isNavLink: true,
|
||||||
item.collapsed = false;
|
exact: true,
|
||||||
}
|
onClick: onItemClick,
|
||||||
|
}
|
||||||
return anyChildItemsActive;
|
: {
|
||||||
}
|
target: '_blank',
|
||||||
|
rel: 'noreferrer noopener',
|
||||||
|
})}
|
||||||
|
{...props}>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DocSidebarItem(props) {
|
||||||
|
switch (props.item.type) {
|
||||||
|
case 'category':
|
||||||
|
return <DocSidebarItemCategory {...props} />;
|
||||||
case 'link':
|
case 'link':
|
||||||
default:
|
default:
|
||||||
return href === path;
|
return <DocSidebarItemLink {...props} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,12 +189,6 @@ function DocSidebar(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sidebarCollapsible) {
|
|
||||||
sidebarData.forEach((sidebarItem) =>
|
|
||||||
mutateSidebarCollapsingState(sidebarItem, path),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames(styles.sidebar, {
|
className={classnames(styles.sidebar, {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue