mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-04 20:57:17 +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;
|
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' {
|
declare module '@theme/NavbarItem/DefaultNavbarItem' {
|
||||||
import type {Props as NavbarNavLinkProps} from '@theme/NavbarItem/NavbarNavLink';
|
import type {Props as NavbarNavLinkProps} from '@theme/NavbarItem/NavbarNavLink';
|
||||||
|
|
||||||
|
@ -758,7 +816,7 @@ declare module '@theme/ColorModeToggle' {
|
||||||
readonly onChange: (colorMode: ColorMode) => void;
|
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' {
|
declare module '@theme/Logo' {
|
||||||
|
|
|
@ -8,19 +8,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
MobileSecondaryMenuFiller,
|
NavbarSecondaryMenuFiller,
|
||||||
type MobileSecondaryMenuComponent,
|
type NavbarSecondaryMenuComponent,
|
||||||
ThemeClassNames,
|
ThemeClassNames,
|
||||||
|
useNavbarMobileSidebar,
|
||||||
} from '@docusaurus/theme-common';
|
} from '@docusaurus/theme-common';
|
||||||
import DocSidebarItems from '@theme/DocSidebarItems';
|
import DocSidebarItems from '@theme/DocSidebarItems';
|
||||||
import type {Props} from '@theme/DocSidebar/Mobile';
|
import type {Props} from '@theme/DocSidebar/Mobile';
|
||||||
|
|
||||||
// eslint-disable-next-line react/function-component-definition
|
// eslint-disable-next-line react/function-component-definition
|
||||||
const DocSidebarMobileSecondaryMenu: MobileSecondaryMenuComponent<Props> = ({
|
const DocSidebarMobileSecondaryMenu: NavbarSecondaryMenuComponent<Props> = ({
|
||||||
toggleSidebar,
|
|
||||||
sidebar,
|
sidebar,
|
||||||
path,
|
path,
|
||||||
}) => (
|
}) => {
|
||||||
|
const mobileSidebar = useNavbarMobileSidebar();
|
||||||
|
return (
|
||||||
<ul className={clsx(ThemeClassNames.docs.docSidebarMenu, 'menu__list')}>
|
<ul className={clsx(ThemeClassNames.docs.docSidebarMenu, 'menu__list')}>
|
||||||
<DocSidebarItems
|
<DocSidebarItems
|
||||||
items={sidebar}
|
items={sidebar}
|
||||||
|
@ -28,20 +30,21 @@ const DocSidebarMobileSecondaryMenu: MobileSecondaryMenuComponent<Props> = ({
|
||||||
onItemClick={(item) => {
|
onItemClick={(item) => {
|
||||||
// Mobile sidebar should only be closed if the category has a link
|
// Mobile sidebar should only be closed if the category has a link
|
||||||
if (item.type === 'category' && item.href) {
|
if (item.type === 'category' && item.href) {
|
||||||
toggleSidebar();
|
mobileSidebar.toggle();
|
||||||
}
|
}
|
||||||
if (item.type === 'link') {
|
if (item.type === 'link') {
|
||||||
toggleSidebar();
|
mobileSidebar.toggle();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
level={1}
|
level={1}
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function DocSidebarMobile(props: Props) {
|
function DocSidebarMobile(props: Props) {
|
||||||
return (
|
return (
|
||||||
<MobileSecondaryMenuFiller
|
<NavbarSecondaryMenuFiller
|
||||||
component={DocSidebarMobileSecondaryMenu}
|
component={DocSidebarMobileSecondaryMenu}
|
||||||
props={props}
|
props={props}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -11,8 +11,8 @@ import {
|
||||||
TabGroupChoiceProvider,
|
TabGroupChoiceProvider,
|
||||||
AnnouncementBarProvider,
|
AnnouncementBarProvider,
|
||||||
DocsPreferredVersionContextProvider,
|
DocsPreferredVersionContextProvider,
|
||||||
MobileSecondaryMenuProvider,
|
|
||||||
ScrollControllerProvider,
|
ScrollControllerProvider,
|
||||||
|
NavbarProvider,
|
||||||
PluginHtmlClassNameProvider,
|
PluginHtmlClassNameProvider,
|
||||||
} from '@docusaurus/theme-common';
|
} from '@docusaurus/theme-common';
|
||||||
import type {Props} from '@theme/LayoutProviders';
|
import type {Props} from '@theme/LayoutProviders';
|
||||||
|
@ -24,11 +24,9 @@ export default function LayoutProviders({children}: Props): JSX.Element {
|
||||||
<TabGroupChoiceProvider>
|
<TabGroupChoiceProvider>
|
||||||
<ScrollControllerProvider>
|
<ScrollControllerProvider>
|
||||||
<DocsPreferredVersionContextProvider>
|
<DocsPreferredVersionContextProvider>
|
||||||
<MobileSecondaryMenuProvider>
|
|
||||||
<PluginHtmlClassNameProvider>
|
<PluginHtmlClassNameProvider>
|
||||||
{children}
|
<NavbarProvider>{children}</NavbarProvider>
|
||||||
</PluginHtmlClassNameProvider>
|
</PluginHtmlClassNameProvider>
|
||||||
</MobileSecondaryMenuProvider>
|
|
||||||
</DocsPreferredVersionContextProvider>
|
</DocsPreferredVersionContextProvider>
|
||||||
</ScrollControllerProvider>
|
</ScrollControllerProvider>
|
||||||
</TabGroupChoiceProvider>
|
</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.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
|
||||||
Hide toggle in small viewports
|
|
||||||
*/
|
|
||||||
@media (max-width: 996px) {
|
|
||||||
.toggle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbarHideable {
|
.navbarHideable {
|
||||||
transition: transform var(--ifm-transition-fast) ease;
|
transition: transform var(--ifm-transition-fast) ease;
|
||||||
}
|
}
|
||||||
|
@ -21,7 +12,3 @@ Hide toggle in small viewports
|
||||||
.navbarHidden {
|
.navbarHidden {
|
||||||
transform: translate3d(0, calc(-100% - 2px), 0);
|
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.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useCallback, useState, useEffect} from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import NavbarLayout from '@theme/Navbar/Layout';
|
||||||
import Translate from '@docusaurus/Translate';
|
import NavbarContent from '@theme/Navbar/Content';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Navbar(): JSX.Element {
|
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 (
|
return (
|
||||||
<nav
|
<NavbarLayout>
|
||||||
ref={navbarRef}
|
<NavbarContent />
|
||||||
className={clsx(
|
</NavbarLayout>
|
||||||
'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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,13 +71,6 @@ export {
|
||||||
|
|
||||||
export {default as Details, type DetailsProps} from './components/Details';
|
export {default as Details, type DetailsProps} from './components/Details';
|
||||||
|
|
||||||
export {
|
|
||||||
MobileSecondaryMenuProvider,
|
|
||||||
MobileSecondaryMenuFiller,
|
|
||||||
useMobileSecondaryMenuRenderer,
|
|
||||||
} from './utils/mobileSecondaryMenu';
|
|
||||||
export type {MobileSecondaryMenuComponent} from './utils/mobileSecondaryMenu';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useDocsPreferredVersion,
|
useDocsPreferredVersion,
|
||||||
useDocsPreferredVersionByPluginId,
|
useDocsPreferredVersionByPluginId,
|
||||||
|
@ -151,6 +144,17 @@ export {
|
||||||
TabGroupChoiceProvider,
|
TabGroupChoiceProvider,
|
||||||
} from './utils/tabGroupChoiceUtils';
|
} 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 useHideableNavbar} from './hooks/useHideableNavbar';
|
||||||
export {
|
export {
|
||||||
default as useKeyboardNavigation,
|
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
|
goss
|
||||||
goyal
|
goyal
|
||||||
gtag
|
gtag
|
||||||
|
hardcoding
|
||||||
hahaha
|
hahaha
|
||||||
héctor
|
héctor
|
||||||
héllô
|
héllô
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"build": "docusaurus build",
|
"build": "docusaurus build",
|
||||||
"swizzle": "docusaurus swizzle",
|
"swizzle": "docusaurus swizzle",
|
||||||
"deploy": "docusaurus deploy",
|
"deploy": "docusaurus deploy",
|
||||||
"clear": "docusaurus clear && rimraf changelog",
|
"clear": "docusaurus clear && rimraf changelog && rimraf _dogfooding/_swizzle_theme_tests",
|
||||||
"serve": "docusaurus serve",
|
"serve": "docusaurus serve",
|
||||||
"test:css-order": "node testCSSOrder.mjs",
|
"test:css-order": "node testCSSOrder.mjs",
|
||||||
"test:swizzle:eject:js": "cross-env SWIZZLE_ACTION='eject' SWIZZLE_TYPESCRIPT='false' node _dogfooding/testSwizzleThemeClassic.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