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:
Sébastien Lorber 2020-06-02 20:04:59 +02:00 committed by GitHub
parent b8de9c6ded
commit 6797af660f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 131 additions and 137 deletions

View file

@ -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'],
},

View file

@ -7,6 +7,7 @@ Object {
"collapsed": true,
"items": Array [
Object {
"collapsed": true,
"items": Array [
Object {
"href": "/docs/foo/bar",

View file

@ -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",

View file

@ -7,6 +7,7 @@ Object {
"collapsed": true,
"items": Array [
Object {
"collapsed": true,
"items": Array [
Object {
"id": "version-1.0.0/foo/bar",

View file

@ -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];

View file

@ -42,7 +42,7 @@ export interface SidebarItemCategory {
type: 'category';
label: string;
items: SidebarItem[];
collapsed?: boolean;
collapsed: boolean;
}
export interface SidebarItemCategoryRaw {

View file

@ -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, {