Skip to content

Commit

Permalink
fix(v2): fix FOUC in doc sidebar and various improvements (#2867)
Browse files Browse the repository at this point in the history
* bug(v2): fix active sidebar item detection logic (#2682 (comment))

* fix sidebar category collapsed normalization to make sure we always have a boolean after normalization

* fix sidebarCollapsible option
  • Loading branch information
slorber committed Jun 2, 2020
1 parent b8de9c6 commit 6797af6
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 137 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Object {
"collapsed": true,
"items": Array [
Object {
"collapsed": true,
"items": Array [
Object {
"href": "/docs/foo/bar",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Object {
"collapsed": true,
"items": Array [
Object {
"collapsed": true,
"items": Array [
Object {
"id": "version-1.0.0/foo/bar",
Expand Down
13 changes: 11 additions & 2 deletions packages/docusaurus-plugin-content-docs/src/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -33,7 +36,7 @@ function normalizeCategoryShorthand(
): SidebarItemCategoryRaw[] {
return Object.entries(sidebar).map(([label, items]) => ({
type: 'category',
collapsed: true,
collapsed: defaultCategoryCollapsedValue,
label,
items,
}));
Expand Down Expand Up @@ -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];
Expand Down
2 changes: 1 addition & 1 deletion packages/docusaurus-plugin-content-docs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export interface SidebarItemCategory {
type: 'category';
label: string;
items: SidebarItem[];
collapsed?: boolean;
collapsed: boolean;
}

export interface SidebarItemCategoryRaw {
Expand Down
245 changes: 116 additions & 129 deletions packages/docusaurus-theme-classic/src/theme/DocSidebar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
};

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>
)
);
const handleItemClick = useCallback(
(e) => {
e.preventDefault();
e.target.blur();
setCollapsed((state) => !state);
},
[setCollapsed],
);

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;
}
}

// 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 (
<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>
);
}

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} />;
}
}

Expand Down Expand Up @@ -196,12 +189,6 @@ function DocSidebar(props) {
);
}

if (sidebarCollapsible) {
sidebarData.forEach((sidebarItem) =>
mutateSidebarCollapsingState(sidebarItem, path),
);
}

return (
<div
className={classnames(styles.sidebar, {
Expand Down

0 comments on commit 6797af6

Please sign in to comment.