refactor: split DocSidebarItem by item type (#7005)

This commit is contained in:
Sébastien Lorber 2022-03-25 18:59:31 +01:00 committed by GitHub
parent 2dea99b5c8
commit 2964e6f65d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 354 additions and 263 deletions

View file

@ -134,6 +134,8 @@ declare module '@docusaurus/plugin-content-docs' {
export type PropSidebarItemLink = export type PropSidebarItemLink =
import('./sidebars/types').PropSidebarItemLink; import('./sidebars/types').PropSidebarItemLink;
export type PropSidebarItemHtml =
import('./sidebars/types').PropSidebarItemHtml;
export type PropSidebarItemCategory = export type PropSidebarItemCategory =
import('./sidebars/types').PropSidebarItemCategory; import('./sidebars/types').PropSidebarItemCategory;
export type PropSidebarItem = import('./sidebars/types').PropSidebarItem; export type PropSidebarItem = import('./sidebars/types').PropSidebarItem;

View file

@ -235,6 +235,40 @@ declare module '@theme/DocSidebarItem' {
export default function DocSidebarItem(props: Props): JSX.Element; export default function DocSidebarItem(props: Props): JSX.Element;
} }
declare module '@theme/DocSidebarItem/Link' {
import type {Props as DocSidebarItemProps} from '@theme/DocSidebarItem';
import type {PropSidebarItemLink} from '@docusaurus/plugin-content-docs';
export interface Props extends DocSidebarItemProps {
item: PropSidebarItemLink;
}
export default function DocSidebarItemLink(props: Props): JSX.Element;
}
declare module '@theme/DocSidebarItem/Html' {
import type {Props as DocSidebarItemProps} from '@theme/DocSidebarItem';
import type {PropSidebarItemHtml} from '@docusaurus/plugin-content-docs';
export interface Props extends DocSidebarItemProps {
item: PropSidebarItemHtml;
}
export default function DocSidebarItemHtml(props: Props): JSX.Element;
}
declare module '@theme/DocSidebarItem/Category' {
import type {Props as DocSidebarItemProps} from '@theme/DocSidebarItem';
import type {PropSidebarItemCategory} from '@docusaurus/plugin-content-docs';
export interface Props extends DocSidebarItemProps {
item: PropSidebarItemCategory;
}
export default function DocSidebarItemCategory(props: Props): JSX.Element;
}
declare module '@theme/DocSidebarItems' { declare module '@theme/DocSidebarItems' {
import type {Props as DocSidebarItemProps} from '@theme/DocSidebarItem'; import type {Props as DocSidebarItemProps} from '@theme/DocSidebarItem';
import type {PropSidebarItem} from '@docusaurus/plugin-content-docs'; import type {PropSidebarItem} from '@docusaurus/plugin-content-docs';

View file

@ -0,0 +1,212 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {type ComponentProps, useEffect, useMemo} from 'react';
import clsx from 'clsx';
import {
isActiveSidebarItem,
usePrevious,
Collapsible,
useCollapsible,
findFirstCategoryLink,
ThemeClassNames,
useThemeConfig,
useDocSidebarItemsExpandedState,
isSamePath,
} from '@docusaurus/theme-common';
import Link from '@docusaurus/Link';
import {translate} from '@docusaurus/Translate';
import DocSidebarItems from '@theme/DocSidebarItems';
import type {Props} from '@theme/DocSidebarItem/Category';
import useIsBrowser from '@docusaurus/useIsBrowser';
// If we navigate to a category and it becomes active, it should automatically
// expand itself
function useAutoExpandActiveCategory({
isActive,
collapsed,
setCollapsed,
}: {
isActive: boolean;
collapsed: boolean;
setCollapsed: (b: boolean) => void;
}) {
const wasActive = usePrevious(isActive);
useEffect(() => {
const justBecameActive = isActive && !wasActive;
if (justBecameActive && collapsed) {
setCollapsed(false);
}
}, [isActive, wasActive, collapsed, setCollapsed]);
}
/**
* When a collapsible category has no link, we still link it to its first child
* during SSR as a temporary fallback. This allows to be able to navigate inside
* the category even when JS fails to load, is delayed or simply disabled
* React hydration becomes an optional progressive enhancement
* see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188
* see https://github.com/facebook/docusaurus/issues/3030
*/
function useCategoryHrefWithSSRFallback(
item: Props['item'],
): string | undefined {
const isBrowser = useIsBrowser();
return useMemo(() => {
if (item.href) {
return item.href;
}
// In these cases, it's not necessary to render a fallback
// We skip the "findFirstCategoryLink" computation
if (isBrowser || !item.collapsible) {
return undefined;
}
return findFirstCategoryLink(item);
}, [item, isBrowser]);
}
function CollapseButton({
categoryLabel,
onClick,
}: {
categoryLabel: string;
onClick: ComponentProps<'button'>['onClick'];
}) {
return (
<button
aria-label={translate(
{
id: 'theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel',
message: "Toggle the collapsible sidebar category '{label}'",
description:
'The ARIA label to toggle the collapsible sidebar category',
},
{label: categoryLabel},
)}
type="button"
className="clean-btn menu__caret"
onClick={onClick}
/>
);
}
export default function DocSidebarItemCategory({
item,
onItemClick,
activePath,
level,
index,
...props
}: Props): JSX.Element {
const {items, label, collapsible, className, href} = item;
const hrefWithSSRFallback = useCategoryHrefWithSSRFallback(item);
const isActive = isActiveSidebarItem(item, activePath);
const isCurrentPage = isSamePath(href, activePath);
const {collapsed, setCollapsed} = useCollapsible({
// active categories are always initialized as expanded
// the default (item.collapsed) is only used for non-active categories
initialState: () => {
if (!collapsible) {
return false;
}
return isActive ? false : item.collapsed;
},
});
useAutoExpandActiveCategory({isActive, collapsed, setCollapsed});
const {expandedItem, setExpandedItem} = useDocSidebarItemsExpandedState();
function updateCollapsed(toCollapsed: boolean = !collapsed) {
setExpandedItem(toCollapsed ? null : index);
setCollapsed(toCollapsed);
}
const {autoCollapseSidebarCategories} = useThemeConfig();
useEffect(() => {
if (
collapsible &&
expandedItem &&
expandedItem !== index &&
autoCollapseSidebarCategories
) {
setCollapsed(true);
}
}, [
collapsible,
expandedItem,
index,
setCollapsed,
autoCollapseSidebarCategories,
]);
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemCategory,
ThemeClassNames.docs.docSidebarItemCategoryLevel(level),
'menu__list-item',
{
'menu__list-item--collapsed': collapsed,
},
className,
)}>
<div
className={clsx('menu__list-item-collapsible', {
'menu__list-item-collapsible--active': isCurrentPage,
})}>
<Link
className={clsx('menu__link', {
'menu__link--sublist': collapsible,
'menu__link--sublist-caret': !href,
'menu__link--active': isActive,
})}
onClick={
collapsible
? (e) => {
onItemClick?.(item);
if (href) {
updateCollapsed(false);
} else {
e.preventDefault();
updateCollapsed();
}
}
: () => {
onItemClick?.(item);
}
}
aria-current={isCurrentPage ? 'page' : undefined}
aria-expanded={collapsible ? !collapsed : undefined}
href={collapsible ? hrefWithSSRFallback ?? '#' : hrefWithSSRFallback}
{...props}>
{label}
</Link>
{href && collapsible && (
<CollapseButton
categoryLabel={label}
onClick={(e) => {
e.preventDefault();
updateCollapsed();
}}
/>
)}
</div>
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
<DocSidebarItems
items={items}
tabIndex={collapsed ? -1 : 0}
onItemClick={onItemClick}
activePath={activePath}
level={level + 1}
/>
</Collapsible>
</li>
);
}

View file

@ -11,7 +11,3 @@
var(--ifm-menu-link-padding-horizontal); var(--ifm-menu-link-padding-horizontal);
} }
} }
.menuExternalLink {
align-items: center;
}

View file

@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import {ThemeClassNames} from '@docusaurus/theme-common';
import type {Props} from '@theme/DocSidebarItem/Html';
import styles from './Html.module.css';
export default function DocSidebarItemHtml({
item,
level,
index,
}: Props): JSX.Element {
const {value, defaultStyle, className} = item;
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemLink,
ThemeClassNames.docs.docSidebarItemLinkLevel(level),
defaultStyle && `${styles.menuHtmlItem} menu__list-item`,
className,
)}
key={index}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{__html: value}}
/>
);
}

View file

@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.menuExternalLink {
align-items: center;
}

View file

@ -0,0 +1,58 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import {isActiveSidebarItem, ThemeClassNames} from '@docusaurus/theme-common';
import Link from '@docusaurus/Link';
import isInternalUrl from '@docusaurus/isInternalUrl';
import IconExternalLink from '@theme/IconExternalLink';
import type {Props} from '@theme/DocSidebarItem/Link';
import styles from './Link.module.css';
export default function DocSidebarItemLink({
item,
onItemClick,
activePath,
level,
index,
...props
}: Props): JSX.Element {
const {href, label, className} = item;
const isActive = isActiveSidebarItem(item, activePath);
const isInternalLink = isInternalUrl(href);
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemLink,
ThemeClassNames.docs.docSidebarItemLinkLevel(level),
'menu__list-item',
className,
)}
key={label}>
<Link
className={clsx(
'menu__link',
!isInternalLink && styles.menuExternalLink,
{
'menu__link--active': isActive,
},
)}
aria-current={isActive ? 'page' : undefined}
to={href}
{...(isInternalLink && {
onClick: onItemClick ? () => onItemClick(item) : undefined,
})}
{...props}>
{label}
{!isInternalLink && <IconExternalLink />}
</Link>
</li>
);
}

View file

@ -5,34 +5,11 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import React, {useEffect, useMemo} from 'react'; import React from 'react';
import clsx from 'clsx'; import DocSidebarItemCategory from '@theme/DocSidebarItem/Category';
import { import DocSidebarItemLink from '@theme/DocSidebarItem/Link';
isActiveSidebarItem, import DocSidebarItemHtml from '@theme/DocSidebarItem/Html';
usePrevious,
Collapsible,
useCollapsible,
findFirstCategoryLink,
ThemeClassNames,
useThemeConfig,
useDocSidebarItemsExpandedState,
isSamePath,
} from '@docusaurus/theme-common';
import Link from '@docusaurus/Link';
import isInternalUrl from '@docusaurus/isInternalUrl';
import {translate} from '@docusaurus/Translate';
import IconExternalLink from '@theme/IconExternalLink';
import DocSidebarItems from '@theme/DocSidebarItems';
import type {Props} from '@theme/DocSidebarItem'; import type {Props} from '@theme/DocSidebarItem';
import type {
PropSidebarItemCategory,
PropSidebarItemLink,
} from '@docusaurus/plugin-content-docs';
import styles from './styles.module.css';
import useIsBrowser from '@docusaurus/useIsBrowser';
import type {SidebarItemHtml} from '@docusaurus/plugin-content-docs/src/sidebars/types';
export default function DocSidebarItem({ export default function DocSidebarItem({
item, item,
@ -48,235 +25,3 @@ export default function DocSidebarItem({
return <DocSidebarItemLink item={item} {...props} />; return <DocSidebarItemLink item={item} {...props} />;
} }
} }
// If we navigate to a category and it becomes active, it should automatically
// expand itself
function useAutoExpandActiveCategory({
isActive,
collapsed,
setCollapsed,
}: {
isActive: boolean;
collapsed: boolean;
setCollapsed: (b: boolean) => void;
}) {
const wasActive = usePrevious(isActive);
useEffect(() => {
const justBecameActive = isActive && !wasActive;
if (justBecameActive && collapsed) {
setCollapsed(false);
}
}, [isActive, wasActive, collapsed, setCollapsed]);
}
/**
* When a collapsible category has no link, we still link it to its first child
* during SSR as a temporary fallback. This allows to be able to navigate inside
* the category even when JS fails to load, is delayed or simply disabled
* React hydration becomes an optional progressive enhancement
* see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188
* see https://github.com/facebook/docusaurus/issues/3030
*/
function useCategoryHrefWithSSRFallback(
item: PropSidebarItemCategory,
): string | undefined {
const isBrowser = useIsBrowser();
return useMemo(() => {
if (item.href) {
return item.href;
}
// In these cases, it's not necessary to render a fallback
// We skip the "findFirstCategoryLink" computation
if (isBrowser || !item.collapsible) {
return undefined;
}
return findFirstCategoryLink(item);
}, [item, isBrowser]);
}
function DocSidebarItemCategory({
item,
onItemClick,
activePath,
level,
index,
...props
}: Props & {item: PropSidebarItemCategory}) {
const {items, label, collapsible, className, href} = item;
const hrefWithSSRFallback = useCategoryHrefWithSSRFallback(item);
const isActive = isActiveSidebarItem(item, activePath);
const isCurrentPage = isSamePath(href, activePath);
const {collapsed, setCollapsed} = useCollapsible({
// active categories are always initialized as expanded
// the default (item.collapsed) is only used for non-active categories
initialState: () => {
if (!collapsible) {
return false;
}
return isActive ? false : item.collapsed;
},
});
useAutoExpandActiveCategory({isActive, collapsed, setCollapsed});
const {expandedItem, setExpandedItem} = useDocSidebarItemsExpandedState();
function updateCollapsed(toCollapsed: boolean = !collapsed) {
setExpandedItem(toCollapsed ? null : index);
setCollapsed(toCollapsed);
}
const {autoCollapseSidebarCategories} = useThemeConfig();
useEffect(() => {
if (
collapsible &&
expandedItem &&
expandedItem !== index &&
autoCollapseSidebarCategories
) {
setCollapsed(true);
}
}, [
collapsible,
expandedItem,
index,
setCollapsed,
autoCollapseSidebarCategories,
]);
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemCategory,
ThemeClassNames.docs.docSidebarItemCategoryLevel(level),
'menu__list-item',
{
'menu__list-item--collapsed': collapsed,
},
className,
)}>
<div
className={clsx('menu__list-item-collapsible', {
'menu__list-item-collapsible--active': isCurrentPage,
})}>
<Link
className={clsx('menu__link', {
'menu__link--sublist': collapsible,
'menu__link--sublist-caret': !href,
'menu__link--active': isActive,
})}
onClick={
collapsible
? (e) => {
onItemClick?.(item);
if (href) {
updateCollapsed(false);
} else {
e.preventDefault();
updateCollapsed();
}
}
: () => {
onItemClick?.(item);
}
}
aria-current={isCurrentPage ? 'page' : undefined}
aria-expanded={collapsible ? !collapsed : undefined}
href={collapsible ? hrefWithSSRFallback ?? '#' : hrefWithSSRFallback}
{...props}>
{label}
</Link>
{href && collapsible && (
<button
aria-label={translate(
{
id: 'theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel',
message: "Toggle the collapsible sidebar category '{label}'",
description:
'The ARIA label to toggle the collapsible sidebar category',
},
{label},
)}
type="button"
className="clean-btn menu__caret"
onClick={(e) => {
e.preventDefault();
updateCollapsed();
}}
/>
)}
</div>
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
<DocSidebarItems
items={items}
tabIndex={collapsed ? -1 : 0}
onItemClick={onItemClick}
activePath={activePath}
level={level + 1}
/>
</Collapsible>
</li>
);
}
function DocSidebarItemHtml({
item,
level,
index,
}: Props & {item: SidebarItemHtml}) {
const {value, defaultStyle, className} = item;
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemLink,
ThemeClassNames.docs.docSidebarItemLinkLevel(level),
defaultStyle && `${styles.menuHtmlItem} menu__list-item`,
className,
)}
key={index}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{__html: value}}
/>
);
}
function DocSidebarItemLink({
item,
onItemClick,
activePath,
level,
index,
...props
}: Props & {item: PropSidebarItemLink}) {
const {href, label, className} = item;
const isActive = isActiveSidebarItem(item, activePath);
const isInternalLink = isInternalUrl(href);
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemLink,
ThemeClassNames.docs.docSidebarItemLinkLevel(level),
'menu__list-item',
className,
)}
key={label}>
<Link
className={clsx(
'menu__link',
!isInternalLink && styles.menuExternalLink,
{
'menu__link--active': isActive,
},
)}
aria-current={isActive ? 'page' : undefined}
to={href}
{...(isInternalLink && {
onClick: onItemClick ? () => onItemClick(item) : undefined,
})}
{...props}>
{label}
{!isInternalLink && <IconExternalLink />}
</Link>
</li>
);
}