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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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