Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(theme-{classic,common}): split navbar into smaller components + cleanup + swizzle config #6895

Merged
merged 18 commits into from
Mar 18, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 59 additions & 1 deletion packages/docusaurus-theme-classic/src/theme-classic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,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';

Expand Down Expand Up @@ -758,7 +816,7 @@ declare module '@theme/ColorModeToggle' {
readonly onChange: (e: SyntheticEvent) => void;
}

export default function Toggle(props: Props): JSX.Element;
export default function ColorModeToggle(props: Props): JSX.Element;
}

declare module '@theme/Logo' {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
TabGroupChoiceProvider,
AnnouncementBarProvider,
DocsPreferredVersionContextProvider,
MobileSecondaryMenuProvider,
NavbarProvider,
ScrollControllerProvider,
} from '@docusaurus/theme-common';
import type {Props} from '@theme/LayoutProviders';
Expand All @@ -23,9 +23,7 @@ export default function LayoutProviders({children}: Props): JSX.Element {
<TabGroupChoiceProvider>
<ScrollControllerProvider>
<DocsPreferredVersionContextProvider>
<MobileSecondaryMenuProvider>
{children}
</MobileSecondaryMenuProvider>
<NavbarProvider>{children}</NavbarProvider>
</DocsPreferredVersionContextProvider>
</ScrollControllerProvider>
</TabGroupChoiceProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Comment on lines +13 to +15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this layer of abstraction is useful? Supposing the entire purpose of ColorModeToggle is to toggle color modes, should we just move the hook calls to ColorModeToggle instead and remove this component?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is value to have design system components (ColorModeToggle) to be very simple and have limited dependencies (ie not using any context). This makes it much easier to test then in isolation, add them to Storybook and other things that I plan to do.

This abstraction is likely to be temporary anyway if we provide a NavbarColorModeItem. For now I assume it's good enough, as this component is not marked as safe anyway

The general idea is that each call site should also be responsible to integrate the toggle on a case-by-case basis. For example, rendering null in case toggle is disabled might not be the best solution in all cases: it really depends on the parent component layout.

If we have later an option to have a sidebar color mode toggle, we'd create a similar SidebarColorModeToggle component, allowing the user to easily have distinct toggle designs for navbar/sidebar

image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, makes sense

const disabled = useThemeConfig().colorMode.disableSwitch;
const {isDarkTheme, toggle} = useColorMode();

if (disabled) {
return null;
}

return (
<ColorModeToggle
className={className}
checked={isDarkTheme}
onChange={toggle}
/>
);
}
Original file line number Diff line number Diff line change
@@ -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?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should do that here, or at least in an immediate follow-up PR. I was asked about how we can change the color mode toggle position and the mobile toggle position. Also in https://docusaurus.canny.io/feature-requests/p/mobile-navbar-toggle-on-righttrailing-edge.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes we should definitely give more flexibility here

It is a potential breaking change and probably requires some work so I'd rather have another follow-up PR to do so, eventually, find a way to be retro compatible (like normalizing the config and auto adding color mode toggle + search)?

About the mobile sidebar toggle, it probably doesn't make much sense to place it in other places than top right/left, but we can also give similar flexibility 🤷‍♂️

<>
{!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 />}
</>
}
/>
);
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,10 @@
* 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;
}

.navbarHidden {
transform: translate3d(0, calc(-100% - 2px), 0);
}

.navbarSidebarToggle {
margin-right: 1rem;
}
Original file line number Diff line number Diff line change
@@ -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"
/>
);
}