mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-02 19:57:25 +02:00
refactor(theme-{classic,common}): split navbar into smaller components + cleanup + swizzle config (#6895)
This commit is contained in:
parent
ecbe0b26c5
commit
a1d333e96b
22 changed files with 808 additions and 456 deletions
|
@ -460,6 +460,64 @@ declare module '@theme/Navbar' {
|
|||
export default function Navbar(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Navbar/ColorModeToggle' {
|
||||
export interface Props {
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
export default function NavbarColorModeToggle(
|
||||
props: Props,
|
||||
): JSX.Element | null;
|
||||
}
|
||||
|
||||
declare module '@theme/Navbar/Logo' {
|
||||
export default function NavbarLogo(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Navbar/Content' {
|
||||
export default function NavbarContent(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Navbar/Layout' {
|
||||
export interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function NavbarLayout(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Navbar/MobileSidebar' {
|
||||
export default function NavbarMobileSidebar(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Navbar/MobileSidebar/Layout' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
interface Props {
|
||||
header: ReactNode;
|
||||
primaryMenu: ReactNode;
|
||||
secondaryMenu: ReactNode;
|
||||
}
|
||||
|
||||
export default function NavbarMobileSidebarLayout(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Navbar/MobileSidebar/Toggle' {
|
||||
export default function NavbarMobileSidebarToggle(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Navbar/MobileSidebar/PrimaryMenu' {
|
||||
export default function NavbarMobileSidebarPrimaryMenu(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Navbar/MobileSidebar/SecondaryMenu' {
|
||||
export default function NavbarMobileSidebarSecondaryMenu(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Navbar/MobileSidebar/Header' {
|
||||
export default function NavbarMobileSidebarHeader(): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/NavbarItem/DefaultNavbarItem' {
|
||||
import type {Props as NavbarNavLinkProps} from '@theme/NavbarItem/NavbarNavLink';
|
||||
|
||||
|
@ -758,7 +816,7 @@ declare module '@theme/ColorModeToggle' {
|
|||
readonly onChange: (colorMode: ColorMode) => void;
|
||||
}
|
||||
|
||||
export default function Toggle(props: Props): JSX.Element;
|
||||
export default function ColorModeToggle(props: Props): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@theme/Logo' {
|
||||
|
|
|
@ -8,40 +8,43 @@
|
|||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
MobileSecondaryMenuFiller,
|
||||
type MobileSecondaryMenuComponent,
|
||||
NavbarSecondaryMenuFiller,
|
||||
type NavbarSecondaryMenuComponent,
|
||||
ThemeClassNames,
|
||||
useNavbarMobileSidebar,
|
||||
} from '@docusaurus/theme-common';
|
||||
import DocSidebarItems from '@theme/DocSidebarItems';
|
||||
import type {Props} from '@theme/DocSidebar/Mobile';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const DocSidebarMobileSecondaryMenu: MobileSecondaryMenuComponent<Props> = ({
|
||||
toggleSidebar,
|
||||
const DocSidebarMobileSecondaryMenu: NavbarSecondaryMenuComponent<Props> = ({
|
||||
sidebar,
|
||||
path,
|
||||
}) => (
|
||||
<ul className={clsx(ThemeClassNames.docs.docSidebarMenu, 'menu__list')}>
|
||||
<DocSidebarItems
|
||||
items={sidebar}
|
||||
activePath={path}
|
||||
onItemClick={(item) => {
|
||||
// Mobile sidebar should only be closed if the category has a link
|
||||
if (item.type === 'category' && item.href) {
|
||||
toggleSidebar();
|
||||
}
|
||||
if (item.type === 'link') {
|
||||
toggleSidebar();
|
||||
}
|
||||
}}
|
||||
level={1}
|
||||
/>
|
||||
</ul>
|
||||
);
|
||||
}) => {
|
||||
const mobileSidebar = useNavbarMobileSidebar();
|
||||
return (
|
||||
<ul className={clsx(ThemeClassNames.docs.docSidebarMenu, 'menu__list')}>
|
||||
<DocSidebarItems
|
||||
items={sidebar}
|
||||
activePath={path}
|
||||
onItemClick={(item) => {
|
||||
// Mobile sidebar should only be closed if the category has a link
|
||||
if (item.type === 'category' && item.href) {
|
||||
mobileSidebar.toggle();
|
||||
}
|
||||
if (item.type === 'link') {
|
||||
mobileSidebar.toggle();
|
||||
}
|
||||
}}
|
||||
level={1}
|
||||
/>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
function DocSidebarMobile(props: Props) {
|
||||
return (
|
||||
<MobileSecondaryMenuFiller
|
||||
<NavbarSecondaryMenuFiller
|
||||
component={DocSidebarMobileSecondaryMenu}
|
||||
props={props}
|
||||
/>
|
||||
|
|
|
@ -11,8 +11,8 @@ import {
|
|||
TabGroupChoiceProvider,
|
||||
AnnouncementBarProvider,
|
||||
DocsPreferredVersionContextProvider,
|
||||
MobileSecondaryMenuProvider,
|
||||
ScrollControllerProvider,
|
||||
NavbarProvider,
|
||||
PluginHtmlClassNameProvider,
|
||||
} from '@docusaurus/theme-common';
|
||||
import type {Props} from '@theme/LayoutProviders';
|
||||
|
@ -24,11 +24,9 @@ export default function LayoutProviders({children}: Props): JSX.Element {
|
|||
<TabGroupChoiceProvider>
|
||||
<ScrollControllerProvider>
|
||||
<DocsPreferredVersionContextProvider>
|
||||
<MobileSecondaryMenuProvider>
|
||||
<PluginHtmlClassNameProvider>
|
||||
{children}
|
||||
</PluginHtmlClassNameProvider>
|
||||
</MobileSecondaryMenuProvider>
|
||||
<PluginHtmlClassNameProvider>
|
||||
<NavbarProvider>{children}</NavbarProvider>
|
||||
</PluginHtmlClassNameProvider>
|
||||
</DocsPreferredVersionContextProvider>
|
||||
</ScrollControllerProvider>
|
||||
</TabGroupChoiceProvider>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* 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 {useColorMode, useThemeConfig} from '@docusaurus/theme-common';
|
||||
import ColorModeToggle from '@theme/ColorModeToggle';
|
||||
import type {Props} from '@theme/Navbar/ColorModeToggle';
|
||||
import React from 'react';
|
||||
|
||||
export default function NavbarColorModeToggle({
|
||||
className,
|
||||
}: Props): JSX.Element | null {
|
||||
const disabled = useThemeConfig().colorMode.disableSwitch;
|
||||
const {colorMode, setColorMode} = useColorMode();
|
||||
|
||||
if (disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ColorModeToggle
|
||||
className={className}
|
||||
value={colorMode}
|
||||
onChange={setColorMode}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* 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 ReactNode} from 'react';
|
||||
import type {Props as NavbarItemConfig} from '@theme/NavbarItem';
|
||||
import NavbarItem from '@theme/NavbarItem';
|
||||
import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle';
|
||||
import SearchBar from '@theme/SearchBar';
|
||||
import {
|
||||
splitNavbarItems,
|
||||
useNavbarMobileSidebar,
|
||||
useThemeConfig,
|
||||
} from '@docusaurus/theme-common';
|
||||
import NavbarMobileSidebarToggle from '@theme/Navbar/MobileSidebar/Toggle';
|
||||
import NavbarLogo from '@theme/Navbar/Logo';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function useNavbarItems() {
|
||||
// TODO temporary casting until ThemeConfig type is improved
|
||||
return useThemeConfig().navbar.items as NavbarItemConfig[];
|
||||
}
|
||||
|
||||
function NavbarItems({items}: {items: NavbarItemConfig[]}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => (
|
||||
<NavbarItem {...item} key={i} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function NavbarContentLayout({
|
||||
left,
|
||||
right,
|
||||
}: {
|
||||
left: ReactNode;
|
||||
right: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="navbar__inner">
|
||||
<div className="navbar__items">{left}</div>
|
||||
<div className="navbar__items navbar__items--right">{right}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NavbarContent(): JSX.Element {
|
||||
const mobileSidebar = useNavbarMobileSidebar();
|
||||
|
||||
const items = useNavbarItems();
|
||||
const [leftItems, rightItems] = splitNavbarItems(items);
|
||||
|
||||
const autoAddSearchBar = !items.some((item) => item.type === 'search');
|
||||
|
||||
return (
|
||||
<NavbarContentLayout
|
||||
left={
|
||||
// TODO stop hardcoding items?
|
||||
<>
|
||||
{!mobileSidebar.disabled && <NavbarMobileSidebarToggle />}
|
||||
<NavbarLogo />
|
||||
<NavbarItems items={leftItems} />
|
||||
</>
|
||||
}
|
||||
right={
|
||||
// TODO stop hardcoding items?
|
||||
// Ask the user to add the respective navbar items => more flexible
|
||||
<>
|
||||
<NavbarItems items={rightItems} />
|
||||
<NavbarColorModeToggle className={styles.colorModeToggle} />
|
||||
{autoAddSearchBar && <SearchBar />}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
Hide color mode toggle in small viewports
|
||||
*/
|
||||
@media (max-width: 996px) {
|
||||
.colorModeToggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* 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} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import NavbarMobileSidebar from '@theme/Navbar/MobileSidebar';
|
||||
import type {Props} from '@theme/Navbar/Layout';
|
||||
import {
|
||||
useThemeConfig,
|
||||
useHideableNavbar,
|
||||
useNavbarMobileSidebar,
|
||||
} from '@docusaurus/theme-common';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function NavbarBackdrop(props: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
role="presentation"
|
||||
{...props}
|
||||
className={clsx('navbar-sidebar__backdrop', props.className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NavbarLayout({children}: Props): JSX.Element {
|
||||
const {
|
||||
navbar: {hideOnScroll, style},
|
||||
} = useThemeConfig();
|
||||
const mobileSidebar = useNavbarMobileSidebar();
|
||||
const {navbarRef, isNavbarVisible} = useHideableNavbar(hideOnScroll);
|
||||
return (
|
||||
<nav
|
||||
ref={navbarRef}
|
||||
className={clsx(
|
||||
'navbar',
|
||||
'navbar--fixed-top',
|
||||
hideOnScroll && [
|
||||
styles.navbarHideable,
|
||||
!isNavbarVisible && styles.navbarHidden,
|
||||
],
|
||||
{
|
||||
'navbar--dark': style === 'dark',
|
||||
'navbar--primary': style === 'primary',
|
||||
'navbar-sidebar--show': mobileSidebar.shown,
|
||||
},
|
||||
)}>
|
||||
{children}
|
||||
<NavbarBackdrop onClick={mobileSidebar.toggle} />
|
||||
<NavbarMobileSidebar />
|
||||
</nav>
|
||||
);
|
||||
}
|
|
@ -5,15 +5,6 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/*
|
||||
Hide toggle in small viewports
|
||||
*/
|
||||
@media (max-width: 996px) {
|
||||
.toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.navbarHideable {
|
||||
transition: transform var(--ifm-transition-fast) ease;
|
||||
}
|
||||
|
@ -21,7 +12,3 @@ Hide toggle in small viewports
|
|||
.navbarHidden {
|
||||
transform: translate3d(0, calc(-100% - 2px), 0);
|
||||
}
|
||||
|
||||
.navbarSidebarToggle {
|
||||
margin-right: 1rem;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* 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 Logo from '@theme/Logo';
|
||||
|
||||
export default function NavbarLogo(): JSX.Element {
|
||||
return (
|
||||
<Logo
|
||||
className="navbar__brand"
|
||||
imageClassName="navbar__logo"
|
||||
titleClassName="navbar__title"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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 NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle';
|
||||
import IconClose from '@theme/IconClose';
|
||||
import NavbarLogo from '@theme/Navbar/Logo';
|
||||
import {useNavbarMobileSidebar} from '@docusaurus/theme-common';
|
||||
|
||||
function CloseButton() {
|
||||
const mobileSidebar = useNavbarMobileSidebar();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="clean-btn navbar-sidebar__close"
|
||||
onClick={() => mobileSidebar.toggle()}>
|
||||
<IconClose color="var(--ifm-color-emphasis-600)" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NavbarMobileSidebarHeader(): JSX.Element {
|
||||
return (
|
||||
<div className="navbar-sidebar__brand">
|
||||
<NavbarLogo />
|
||||
<NavbarColorModeToggle className="margin-right--md" />
|
||||
<CloseButton />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* 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 type {Props} from '@theme/Navbar/MobileSidebar/Layout';
|
||||
import {useNavbarSecondaryMenu} from '@docusaurus/theme-common';
|
||||
|
||||
export default function NavbarMobileSidebarLayout({
|
||||
header,
|
||||
primaryMenu,
|
||||
secondaryMenu,
|
||||
}: Props): JSX.Element {
|
||||
const {shown: secondaryMenuShown} = useNavbarSecondaryMenu();
|
||||
return (
|
||||
<div className="navbar-sidebar">
|
||||
{header}
|
||||
<div
|
||||
className={clsx('navbar-sidebar__items', {
|
||||
'navbar-sidebar__items--show-secondary': secondaryMenuShown,
|
||||
})}>
|
||||
<div className="navbar-sidebar__item menu">{primaryMenu}</div>
|
||||
<div className="navbar-sidebar__item menu">{secondaryMenu}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* 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 {useNavbarMobileSidebar, useThemeConfig} from '@docusaurus/theme-common';
|
||||
import type {Props as NavbarItemConfig} from '@theme/NavbarItem';
|
||||
import NavbarItem from '../../../NavbarItem';
|
||||
|
||||
function useNavbarItems() {
|
||||
// TODO temporary casting until ThemeConfig type is improved
|
||||
return useThemeConfig().navbar.items as NavbarItemConfig[];
|
||||
}
|
||||
|
||||
// The primary menu displays the navbar items
|
||||
export default function NavbarMobilePrimaryMenu(): JSX.Element {
|
||||
const mobileSidebar = useNavbarMobileSidebar();
|
||||
|
||||
// TODO how can the order be defined for mobile?
|
||||
// Should we allow providing a different list of items?
|
||||
const items = useNavbarItems();
|
||||
|
||||
return (
|
||||
<ul className="menu__list">
|
||||
{items.map((item, i) => (
|
||||
<NavbarItem
|
||||
mobile
|
||||
{...item}
|
||||
onClick={() => mobileSidebar.toggle()}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* 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} from 'react';
|
||||
import {useNavbarSecondaryMenu, useThemeConfig} from '@docusaurus/theme-common';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
|
||||
function SecondaryMenuBackButton(props: ComponentProps<'button'>) {
|
||||
return (
|
||||
<button {...props} type="button" className="clean-btn navbar-sidebar__back">
|
||||
<Translate
|
||||
id="theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel"
|
||||
description="The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)">
|
||||
← Back to main menu
|
||||
</Translate>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// The secondary menu slides from the right and shows contextual information
|
||||
// such as the docs sidebar
|
||||
export default function NavbarMobileSidebarSecondaryMenu(): JSX.Element | null {
|
||||
const isPrimaryMenuEmpty = useThemeConfig().navbar.items.length === 0;
|
||||
const secondaryMenu = useNavbarSecondaryMenu();
|
||||
return (
|
||||
<>
|
||||
{/* edge-case: prevent returning to the primaryMenu when it's empty */}
|
||||
{!isPrimaryMenuEmpty && (
|
||||
<SecondaryMenuBackButton onClick={() => secondaryMenu.hide()} />
|
||||
)}
|
||||
{secondaryMenu.content}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* 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 IconMenu from '@theme/IconMenu';
|
||||
import {useNavbarMobileSidebar} from '@docusaurus/theme-common';
|
||||
|
||||
export default function MobileSidebarToggle(): JSX.Element {
|
||||
const mobileSidebar = useNavbarMobileSidebar();
|
||||
return (
|
||||
<button
|
||||
onClick={mobileSidebar.toggle}
|
||||
onKeyDown={mobileSidebar.toggle}
|
||||
aria-label="Navigation bar toggle"
|
||||
className="navbar__toggle clean-btn"
|
||||
type="button"
|
||||
tabIndex={0}>
|
||||
<IconMenu />
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* 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 NavbarMobileSidebarLayout from '@theme/Navbar/MobileSidebar/Layout';
|
||||
import NavbarMobileSidebarHeader from '@theme/Navbar/MobileSidebar/Header';
|
||||
import {
|
||||
useLockBodyScroll,
|
||||
useNavbarMobileSidebar,
|
||||
} from '@docusaurus/theme-common';
|
||||
import NavbarMobileSidebarPrimaryMenu from '@theme/Navbar/MobileSidebar/PrimaryMenu';
|
||||
import NavbarMobileSidebarSecondaryMenu from '@theme/Navbar/MobileSidebar/SecondaryMenu';
|
||||
|
||||
export default function NavbarMobileSidebar(): JSX.Element | null {
|
||||
const mobileSidebar = useNavbarMobileSidebar();
|
||||
useLockBodyScroll(mobileSidebar.shown);
|
||||
|
||||
if (!mobileSidebar.shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavbarMobileSidebarLayout
|
||||
header={<NavbarMobileSidebarHeader />}
|
||||
primaryMenu={<NavbarMobileSidebarPrimaryMenu />}
|
||||
secondaryMenu={<NavbarMobileSidebarSecondaryMenu />}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -5,300 +5,14 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useState, useEffect} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import SearchBar from '@theme/SearchBar';
|
||||
import ColorModeToggle from '@theme/ColorModeToggle';
|
||||
import {
|
||||
useThemeConfig,
|
||||
useMobileSecondaryMenuRenderer,
|
||||
usePrevious,
|
||||
useHistoryPopHandler,
|
||||
useHideableNavbar,
|
||||
useLockBodyScroll,
|
||||
useWindowSize,
|
||||
useColorMode,
|
||||
} from '@docusaurus/theme-common';
|
||||
import {useActivePlugin} from '@docusaurus/plugin-content-docs/client';
|
||||
import NavbarItem, {type Props as NavbarItemConfig} from '@theme/NavbarItem';
|
||||
import Logo from '@theme/Logo';
|
||||
import IconMenu from '@theme/IconMenu';
|
||||
import IconClose from '@theme/IconClose';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
// retrocompatible with v1
|
||||
const DefaultNavItemPosition = 'right';
|
||||
|
||||
function useNavbarItems() {
|
||||
// TODO temporary casting until ThemeConfig type is improved
|
||||
return useThemeConfig().navbar.items as NavbarItemConfig[];
|
||||
}
|
||||
|
||||
// If split links by left/right
|
||||
// if position is unspecified, fallback to right (as v1)
|
||||
function splitNavItemsByPosition(items: NavbarItemConfig[]) {
|
||||
const leftItems = items.filter(
|
||||
(item) => (item.position ?? DefaultNavItemPosition) === 'left',
|
||||
);
|
||||
const rightItems = items.filter(
|
||||
(item) => (item.position ?? DefaultNavItemPosition) === 'right',
|
||||
);
|
||||
return {
|
||||
leftItems,
|
||||
rightItems,
|
||||
};
|
||||
}
|
||||
|
||||
function useMobileSidebar() {
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
// Mobile sidebar not visible on hydration: can avoid SSR rendering
|
||||
const shouldRender = windowSize === 'mobile'; // || windowSize === 'ssr';
|
||||
|
||||
const [shown, setShown] = useState(false);
|
||||
|
||||
// Close mobile sidebar on navigation pop
|
||||
// Most likely firing when using the Android back button (but not only)
|
||||
useHistoryPopHandler(() => {
|
||||
if (shown) {
|
||||
setShown(false);
|
||||
// Should we prevent the navigation here?
|
||||
// See https://github.com/facebook/docusaurus/pull/5462#issuecomment-911699846
|
||||
return false; // prevent pop navigation
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setShown((s) => !s);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (windowSize === 'desktop') {
|
||||
setShown(false);
|
||||
}
|
||||
}, [windowSize]);
|
||||
|
||||
return {shouldRender, toggle, shown};
|
||||
}
|
||||
|
||||
function useColorModeToggle() {
|
||||
const {
|
||||
colorMode: {disableSwitch},
|
||||
} = useThemeConfig();
|
||||
const {colorMode, setColorMode} = useColorMode();
|
||||
return {
|
||||
value: colorMode,
|
||||
onChange: setColorMode,
|
||||
disabled: disableSwitch,
|
||||
};
|
||||
}
|
||||
|
||||
function useSecondaryMenu({
|
||||
sidebarShown,
|
||||
toggleSidebar,
|
||||
}: NavbarMobileSidebarProps) {
|
||||
const content = useMobileSecondaryMenuRenderer()?.({
|
||||
toggleSidebar,
|
||||
});
|
||||
const previousContent = usePrevious(content);
|
||||
|
||||
const [shown, setShown] = useState<boolean>(
|
||||
() =>
|
||||
// /!\ content is set with useEffect,
|
||||
// so it's not available on mount anyway
|
||||
// "return !!content" => always returns false
|
||||
false,
|
||||
);
|
||||
|
||||
// When content is become available for the first time (set in useEffect)
|
||||
// we set this content to be shown!
|
||||
useEffect(() => {
|
||||
const contentBecameAvailable = content && !previousContent;
|
||||
if (contentBecameAvailable) {
|
||||
setShown(true);
|
||||
}
|
||||
}, [content, previousContent]);
|
||||
|
||||
const hasContent = !!content;
|
||||
|
||||
// On sidebar close, secondary menu is set to be shown on next re-opening
|
||||
// (if any secondary menu content available)
|
||||
useEffect(() => {
|
||||
if (!hasContent) {
|
||||
setShown(false);
|
||||
return;
|
||||
}
|
||||
if (!sidebarShown) {
|
||||
setShown(true);
|
||||
}
|
||||
}, [sidebarShown, hasContent]);
|
||||
|
||||
const hide = useCallback(() => {
|
||||
setShown(false);
|
||||
}, []);
|
||||
|
||||
return {shown, hide, content};
|
||||
}
|
||||
|
||||
type NavbarMobileSidebarProps = {
|
||||
sidebarShown: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
function NavbarMobileSidebar({
|
||||
sidebarShown,
|
||||
toggleSidebar,
|
||||
}: NavbarMobileSidebarProps) {
|
||||
useLockBodyScroll(sidebarShown);
|
||||
const items = useNavbarItems();
|
||||
|
||||
const colorModeToggle = useColorModeToggle();
|
||||
|
||||
const secondaryMenu = useSecondaryMenu({
|
||||
sidebarShown,
|
||||
toggleSidebar,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="navbar-sidebar">
|
||||
<div className="navbar-sidebar__brand">
|
||||
<Logo
|
||||
className="navbar__brand"
|
||||
imageClassName="navbar__logo"
|
||||
titleClassName="navbar__title"
|
||||
/>
|
||||
{!colorModeToggle.disabled && (
|
||||
<ColorModeToggle
|
||||
className={styles.navbarSidebarToggle}
|
||||
value={colorModeToggle.value}
|
||||
onChange={colorModeToggle.onChange}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="clean-btn navbar-sidebar__close"
|
||||
onClick={toggleSidebar}>
|
||||
<IconClose
|
||||
color="var(--ifm-color-emphasis-600)"
|
||||
className={styles.navbarSidebarCloseSvg}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx('navbar-sidebar__items', {
|
||||
'navbar-sidebar__items--show-secondary': secondaryMenu.shown,
|
||||
})}>
|
||||
<div className="navbar-sidebar__item menu">
|
||||
<ul className="menu__list">
|
||||
{items.map((item, i) => (
|
||||
<NavbarItem mobile {...item} onClick={toggleSidebar} key={i} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="navbar-sidebar__item menu">
|
||||
{items.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="clean-btn navbar-sidebar__back"
|
||||
onClick={secondaryMenu.hide}>
|
||||
<Translate
|
||||
id="theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel"
|
||||
description="The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)">
|
||||
← Back to main menu
|
||||
</Translate>
|
||||
</button>
|
||||
)}
|
||||
{secondaryMenu.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import React from 'react';
|
||||
import NavbarLayout from '@theme/Navbar/Layout';
|
||||
import NavbarContent from '@theme/Navbar/Content';
|
||||
|
||||
export default function Navbar(): JSX.Element {
|
||||
const {
|
||||
navbar: {hideOnScroll, style},
|
||||
} = useThemeConfig();
|
||||
|
||||
const mobileSidebar = useMobileSidebar();
|
||||
const colorModeToggle = useColorModeToggle();
|
||||
const activeDocPlugin = useActivePlugin();
|
||||
const {navbarRef, isNavbarVisible} = useHideableNavbar(hideOnScroll);
|
||||
|
||||
const items = useNavbarItems();
|
||||
const hasSearchNavbarItem = items.some((item) => item.type === 'search');
|
||||
const {leftItems, rightItems} = splitNavItemsByPosition(items);
|
||||
|
||||
return (
|
||||
<nav
|
||||
ref={navbarRef}
|
||||
className={clsx(
|
||||
'navbar',
|
||||
'navbar--fixed-top',
|
||||
hideOnScroll && [
|
||||
styles.navbarHideable,
|
||||
!isNavbarVisible && styles.navbarHidden,
|
||||
],
|
||||
{
|
||||
'navbar--dark': style === 'dark',
|
||||
'navbar--primary': style === 'primary',
|
||||
'navbar-sidebar--show': mobileSidebar.shown,
|
||||
},
|
||||
)}>
|
||||
<div className="navbar__inner">
|
||||
<div className="navbar__items">
|
||||
{(items?.length > 0 || activeDocPlugin) && (
|
||||
<button
|
||||
aria-label="Navigation bar toggle"
|
||||
className="navbar__toggle clean-btn"
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={mobileSidebar.toggle}
|
||||
onKeyDown={mobileSidebar.toggle}>
|
||||
<IconMenu />
|
||||
</button>
|
||||
)}
|
||||
<Logo
|
||||
className="navbar__brand"
|
||||
imageClassName="navbar__logo"
|
||||
titleClassName="navbar__title"
|
||||
/>
|
||||
{leftItems.map((item, i) => (
|
||||
<NavbarItem {...item} key={i} />
|
||||
))}
|
||||
</div>
|
||||
<div className="navbar__items navbar__items--right">
|
||||
{rightItems.map((item, i) => (
|
||||
<NavbarItem {...item} key={i} />
|
||||
))}
|
||||
{!colorModeToggle.disabled && (
|
||||
<ColorModeToggle
|
||||
className={styles.toggle}
|
||||
value={colorModeToggle.value}
|
||||
onChange={colorModeToggle.onChange}
|
||||
/>
|
||||
)}
|
||||
{!hasSearchNavbarItem && <SearchBar />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
role="presentation"
|
||||
className="navbar-sidebar__backdrop"
|
||||
onClick={mobileSidebar.toggle}
|
||||
/>
|
||||
|
||||
{mobileSidebar.shouldRender && (
|
||||
<NavbarMobileSidebar
|
||||
sidebarShown={mobileSidebar.shown}
|
||||
toggleSidebar={mobileSidebar.toggle}
|
||||
/>
|
||||
)}
|
||||
</nav>
|
||||
<NavbarLayout>
|
||||
<NavbarContent />
|
||||
</NavbarLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -71,13 +71,6 @@ export {
|
|||
|
||||
export {default as Details, type DetailsProps} from './components/Details';
|
||||
|
||||
export {
|
||||
MobileSecondaryMenuProvider,
|
||||
MobileSecondaryMenuFiller,
|
||||
useMobileSecondaryMenuRenderer,
|
||||
} from './utils/mobileSecondaryMenu';
|
||||
export type {MobileSecondaryMenuComponent} from './utils/mobileSecondaryMenu';
|
||||
|
||||
export {
|
||||
useDocsPreferredVersion,
|
||||
useDocsPreferredVersionByPluginId,
|
||||
|
@ -151,6 +144,17 @@ export {
|
|||
TabGroupChoiceProvider,
|
||||
} from './utils/tabGroupChoiceUtils';
|
||||
|
||||
export {
|
||||
splitNavbarItems,
|
||||
NavbarProvider,
|
||||
useNavbarMobileSidebar,
|
||||
} from './utils/navbarUtils';
|
||||
export {
|
||||
useNavbarSecondaryMenu,
|
||||
NavbarSecondaryMenuFiller,
|
||||
} from './utils/navbarSecondaryMenuUtils';
|
||||
export type {NavbarSecondaryMenuComponent} from './utils/navbarSecondaryMenuUtils';
|
||||
|
||||
export {default as useHideableNavbar} from './hooks/useHideableNavbar';
|
||||
export {
|
||||
default as useKeyboardNavigation,
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
/**
|
||||
* 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, {
|
||||
useState,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
type ReactNode,
|
||||
type ComponentType,
|
||||
} from 'react';
|
||||
import {ReactContextError} from './reactUtils';
|
||||
|
||||
/*
|
||||
The idea behind all this is that a specific component must be able to fill a
|
||||
placeholder in the generic layout. The doc page should be able to fill the
|
||||
secondary menu of the main mobile navbar. This permits to reduce coupling
|
||||
between the main layout and the specific page.
|
||||
|
||||
This kind of feature is often called portal/teleport/gateway... various
|
||||
unmaintained React libs exist. Most up-to-date one: https://github.com/gregberge/react-teleporter
|
||||
Not sure any of those is safe regarding concurrent mode.
|
||||
*/
|
||||
|
||||
type ExtraProps = {
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
export type MobileSecondaryMenuComponent<Props> = ComponentType<
|
||||
Props & ExtraProps
|
||||
>;
|
||||
|
||||
type State = {
|
||||
component: MobileSecondaryMenuComponent<unknown>;
|
||||
props: unknown;
|
||||
} | null;
|
||||
|
||||
function useContextValue() {
|
||||
return useState<State>(null);
|
||||
}
|
||||
|
||||
type ContextValue = ReturnType<typeof useContextValue>;
|
||||
|
||||
const Context = React.createContext<ContextValue | null>(null);
|
||||
|
||||
export function MobileSecondaryMenuProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Context.Provider value={useContextValue()}>{children}</Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useMobileSecondaryMenuContext(): ContextValue {
|
||||
const value = useContext(Context);
|
||||
if (value === null) {
|
||||
throw new ReactContextError('MobileSecondaryMenuProvider');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function useMobileSecondaryMenuRenderer(): (
|
||||
extraProps: ExtraProps,
|
||||
) => ReactNode | undefined {
|
||||
const [state] = useMobileSecondaryMenuContext();
|
||||
if (state) {
|
||||
const Comp = state.component;
|
||||
return function render(extraProps) {
|
||||
return <Comp {...state.props} {...extraProps} />;
|
||||
};
|
||||
}
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
function useShallowMemoizedObject<O extends Record<string, unknown>>(obj: O) {
|
||||
return useMemo(
|
||||
() => obj,
|
||||
// Is this safe?
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[...Object.keys(obj), ...Object.values(obj)],
|
||||
);
|
||||
}
|
||||
|
||||
// Fill the secondary menu placeholder with some real content
|
||||
export function MobileSecondaryMenuFiller<
|
||||
Props extends Record<string, unknown>,
|
||||
>({
|
||||
component,
|
||||
props,
|
||||
}: {
|
||||
component: MobileSecondaryMenuComponent<Props & ExtraProps>;
|
||||
props: Props;
|
||||
}): JSX.Element | null {
|
||||
const [, setState] = useMobileSecondaryMenuContext();
|
||||
|
||||
// To avoid useless context re-renders, props are memoized shallowly
|
||||
const memoizedProps = useShallowMemoizedObject(props);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-expect-error: context is not 100% type-safe but it's ok
|
||||
setState({component, props: memoizedProps});
|
||||
}, [setState, component, memoizedProps]);
|
||||
|
||||
useEffect(() => () => setState(null), [setState]);
|
||||
|
||||
return null;
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* 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, {
|
||||
useState,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
type ReactNode,
|
||||
type ComponentType,
|
||||
} from 'react';
|
||||
import {ReactContextError} from './reactUtils';
|
||||
import {usePrevious} from './usePrevious';
|
||||
import {useNavbarMobileSidebar} from './navbarUtils';
|
||||
|
||||
/*
|
||||
The idea behind all this is that a specific component must be able to fill a
|
||||
placeholder in the generic layout. The doc page should be able to fill the
|
||||
secondary menu of the main mobile navbar. This permits to reduce coupling
|
||||
between the main layout and the specific page.
|
||||
|
||||
This kind of feature is often called portal/teleport/gateway... various
|
||||
unmaintained React libs exist. Most up-to-date one: https://github.com/gregberge/react-teleporter
|
||||
Not sure any of those is safe regarding concurrent mode.
|
||||
*/
|
||||
|
||||
export type NavbarSecondaryMenuComponent<Props> = ComponentType<Props>;
|
||||
|
||||
type State = {
|
||||
shown: boolean;
|
||||
content:
|
||||
| {
|
||||
component: ComponentType<object>;
|
||||
props: object;
|
||||
}
|
||||
| {component: null; props: null};
|
||||
};
|
||||
|
||||
const InitialState: State = {
|
||||
shown: false,
|
||||
content: {component: null, props: null},
|
||||
};
|
||||
|
||||
function useContextValue() {
|
||||
const mobileSidebar = useNavbarMobileSidebar();
|
||||
|
||||
const [state, setState] = useState<State>(InitialState);
|
||||
|
||||
const setShown = (shown: boolean) => setState((s) => ({...s, shown}));
|
||||
|
||||
const hasContent = state.content?.component !== null;
|
||||
const previousHasContent = usePrevious(state.content?.component !== null);
|
||||
|
||||
// When content is become available for the first time (set in useEffect)
|
||||
// we set this content to be shown!
|
||||
useEffect(() => {
|
||||
const contentBecameAvailable = hasContent && !previousHasContent;
|
||||
if (contentBecameAvailable) {
|
||||
setShown(true);
|
||||
}
|
||||
}, [hasContent, previousHasContent]);
|
||||
|
||||
// On sidebar close, secondary menu is set to be shown on next re-opening
|
||||
// (if any secondary menu content available)
|
||||
useEffect(() => {
|
||||
if (!hasContent) {
|
||||
setShown(false);
|
||||
return;
|
||||
}
|
||||
if (!mobileSidebar.shown) {
|
||||
setShown(true);
|
||||
}
|
||||
}, [mobileSidebar.shown, hasContent]);
|
||||
|
||||
return [state, setState] as const;
|
||||
}
|
||||
|
||||
type ContextValue = ReturnType<typeof useContextValue>;
|
||||
|
||||
const Context = React.createContext<ContextValue | null>(null);
|
||||
|
||||
export function NavbarSecondaryMenuProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Context.Provider value={useContextValue()}>{children}</Context.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useNavbarSecondaryMenuContext(): ContextValue {
|
||||
const value = useContext(Context);
|
||||
if (value === null) {
|
||||
throw new ReactContextError('MobileSecondaryMenuProvider');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function useShallowMemoizedObject<O extends Record<string, unknown>>(obj: O) {
|
||||
return useMemo(
|
||||
() => obj,
|
||||
// Is this safe?
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[...Object.keys(obj), ...Object.values(obj)],
|
||||
);
|
||||
}
|
||||
|
||||
// Fill the secondary menu placeholder with some real content
|
||||
export function NavbarSecondaryMenuFiller<
|
||||
Props extends Record<string, unknown>,
|
||||
>({
|
||||
component,
|
||||
props,
|
||||
}: {
|
||||
component: NavbarSecondaryMenuComponent<Props>;
|
||||
props: Props;
|
||||
}): JSX.Element | null {
|
||||
const [, setState] = useNavbarSecondaryMenuContext();
|
||||
|
||||
// To avoid useless context re-renders, props are memoized shallowly
|
||||
const memoizedProps = useShallowMemoizedObject(props);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-expect-error: context is not 100% type-safe but it's ok
|
||||
setState((s) => ({...s, content: {component, props: memoizedProps}}));
|
||||
}, [setState, component, memoizedProps]);
|
||||
|
||||
useEffect(
|
||||
() => () => setState((s) => ({...s, component: null, props: null})),
|
||||
[setState],
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderElement(state: State): JSX.Element | undefined {
|
||||
if (state.content?.component) {
|
||||
const Comp = state.content.component;
|
||||
return <Comp {...state.content.props} />;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function useNavbarSecondaryMenu(): {
|
||||
shown: boolean;
|
||||
hide: () => void;
|
||||
content: JSX.Element | undefined;
|
||||
} {
|
||||
const [state, setState] = useNavbarSecondaryMenuContext();
|
||||
|
||||
const hide = useCallback(
|
||||
() => setState((s) => ({...s, shown: false})),
|
||||
[setState],
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
shown: state.shown,
|
||||
hide,
|
||||
content: renderElement(state),
|
||||
}),
|
||||
[hide, state],
|
||||
);
|
||||
}
|
129
packages/docusaurus-theme-common/src/utils/navbarUtils.tsx
Normal file
129
packages/docusaurus-theme-common/src/utils/navbarUtils.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* 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 ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import useWindowSize from '../hooks/useWindowSize';
|
||||
import {useHistoryPopHandler} from './historyUtils';
|
||||
import {NavbarSecondaryMenuProvider} from './navbarSecondaryMenuUtils';
|
||||
import {useActivePlugin} from '@docusaurus/plugin-content-docs/client';
|
||||
import {useThemeConfig} from './useThemeConfig';
|
||||
import {ReactContextError} from './reactUtils';
|
||||
|
||||
const DefaultNavItemPosition = 'right';
|
||||
|
||||
// If split links by left/right
|
||||
// if position is unspecified, fallback to right
|
||||
export function splitNavbarItems<T extends {position?: 'left' | 'right'}>(
|
||||
items: T[],
|
||||
): [leftItems: T[], rightItems: T[]] {
|
||||
function isLeft(item: T): boolean {
|
||||
return (item.position ?? DefaultNavItemPosition) === 'left';
|
||||
}
|
||||
|
||||
const leftItems = items.filter(isLeft);
|
||||
const rightItems = items.filter((item) => !isLeft(item));
|
||||
|
||||
return [leftItems, rightItems];
|
||||
}
|
||||
|
||||
type NavbarMobileSidebarContextValue = {
|
||||
disabled: boolean;
|
||||
shouldRender: boolean;
|
||||
toggle: () => void;
|
||||
shown: boolean;
|
||||
};
|
||||
|
||||
const NavbarMobileSidebarContext = React.createContext<
|
||||
NavbarMobileSidebarContextValue | undefined
|
||||
>(undefined);
|
||||
|
||||
// Mobile sidebar can be disabled in case it would lead to an empty sidebar
|
||||
// In this case it's not useful to display a navbar sidebar toggle button
|
||||
function useNavbarMobileSidebarDisabled() {
|
||||
const activeDocPlugin = useActivePlugin();
|
||||
const {items} = useThemeConfig().navbar;
|
||||
return items.length === 0 && !activeDocPlugin;
|
||||
}
|
||||
|
||||
function useNavbarMobileSidebarContextValue(): NavbarMobileSidebarContextValue {
|
||||
const disabled = useNavbarMobileSidebarDisabled();
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
// Mobile sidebar not visible until user interaction: can avoid SSR rendering
|
||||
const shouldRender = !disabled && windowSize === 'mobile'; // || windowSize === 'ssr';
|
||||
|
||||
const [shown, setShown] = useState(false);
|
||||
|
||||
// Close mobile sidebar on navigation pop
|
||||
// Most likely firing when using the Android back button (but not only)
|
||||
useHistoryPopHandler(() => {
|
||||
if (shown) {
|
||||
setShown(false);
|
||||
// Should we prevent the navigation here?
|
||||
// See https://github.com/facebook/docusaurus/pull/5462#issuecomment-911699846
|
||||
return false; // prevent pop navigation
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setShown((s) => !s);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (windowSize === 'desktop') {
|
||||
setShown(false);
|
||||
}
|
||||
}, [windowSize]);
|
||||
|
||||
// Return stable context value
|
||||
return useMemo(
|
||||
() => ({
|
||||
disabled,
|
||||
shouldRender,
|
||||
toggle,
|
||||
shown,
|
||||
}),
|
||||
[disabled, shouldRender, toggle, shown],
|
||||
);
|
||||
}
|
||||
|
||||
function NavbarMobileSidebarProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
const value = useNavbarMobileSidebarContextValue();
|
||||
return (
|
||||
<NavbarMobileSidebarContext.Provider value={value}>
|
||||
{children}
|
||||
</NavbarMobileSidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useNavbarMobileSidebar(): NavbarMobileSidebarContextValue {
|
||||
const context = React.useContext(NavbarMobileSidebarContext);
|
||||
if (context == null) {
|
||||
throw new ReactContextError('NavbarMobileSidebarProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// Add all Navbar providers at once
|
||||
export function NavbarProvider({children}: {children: ReactNode}): JSX.Element {
|
||||
return (
|
||||
<NavbarMobileSidebarProvider>
|
||||
<NavbarSecondaryMenuProvider>{children}</NavbarSecondaryMenuProvider>
|
||||
</NavbarMobileSidebarProvider>
|
||||
);
|
||||
}
|
|
@ -98,6 +98,7 @@ globby
|
|||
goss
|
||||
goyal
|
||||
gtag
|
||||
hardcoding
|
||||
hahaha
|
||||
héctor
|
||||
héllô
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear && rimraf changelog",
|
||||
"clear": "docusaurus clear && rimraf changelog && rimraf _dogfooding/_swizzle_theme_tests",
|
||||
"serve": "docusaurus serve",
|
||||
"test:css-order": "node testCSSOrder.mjs",
|
||||
"test:swizzle:eject:js": "cross-env SWIZZLE_ACTION='eject' SWIZZLE_TYPESCRIPT='false' node _dogfooding/testSwizzleThemeClassic.mjs",
|
||||
|
|
Loading…
Add table
Reference in a new issue