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

feat(v2): allow any type of NavbarItem to be passed in dropdown #5072

Merged
merged 43 commits into from
Jul 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ec200db
Initial work
Josh-Cena Jun 26, 2021
61081b8
More elegant `eslint-disable`
Josh-Cena Jun 27, 2021
0643643
Fix typing
Josh-Cena Jun 28, 2021
71b486f
Allow doc links in dropdown
Josh-Cena Jun 28, 2021
ddd4ae6
Allow more dropdowns to use linklike items
Josh-Cena Jun 28, 2021
adc47b1
Finalize
Josh-Cena Jun 28, 2021
9c69f97
Dogfood
Josh-Cena Jun 28, 2021
8fae628
Disallow nested dropdowns
Josh-Cena Jun 28, 2021
79c7e92
Better typing
Josh-Cena Jun 29, 2021
80815f9
Complete type fix
Josh-Cena Jun 29, 2021
7e7de27
Use flatmap
Josh-Cena Jun 29, 2021
10acb67
Patch
Josh-Cena Jun 29, 2021
33cda97
Test
Josh-Cena Jun 29, 2021
1d62f1d
Try fix
Josh-Cena Jun 29, 2021
48fd04a
Style change
Josh-Cena Jun 29, 2021
068dd3f
Revert (to test)
Josh-Cena Jun 30, 2021
a133ec2
Hmmm
Josh-Cena Jun 30, 2021
f3901e6
I know what's wrong
Josh-Cena Jun 30, 2021
24b162e
Does this work?
Josh-Cena Jun 30, 2021
2131e00
Nope
Josh-Cena Jun 30, 2021
70ca5d0
Wrong class name
Josh-Cena Jun 30, 2021
a779adb
Pass prop to render dropdown item differently
Josh-Cena Jun 30, 2021
8c07257
This looks better
Josh-Cena Jun 30, 2021
fdf58d9
Tests passed
Josh-Cena Jun 30, 2021
c85fca9
Separate dropdown from default
Josh-Cena Jul 2, 2021
2153a14
Pourquois?
Josh-Cena Jul 2, 2021
db74fbe
Better prop typing
Josh-Cena Jul 2, 2021
618df82
Make code simpler
Josh-Cena Jul 2, 2021
27220a0
There's some extra className
Josh-Cena Jul 2, 2021
3338b79
Test
Josh-Cena Jul 2, 2021
2263083
More test
Josh-Cena Jul 2, 2021
4b9e171
A-ha!
Josh-Cena Jul 2, 2021
dacf15f
Test again?
Josh-Cena Jul 2, 2021
559a7bf
Add backward compatibility
Josh-Cena Jul 2, 2021
52d0aac
Merge branch 'master' into navbaritem
Josh-Cena Jul 10, 2021
c7ccd7c
Incorporate my type fix
Josh-Cena Jul 10, 2021
35b1b99
Resolve conflict
Josh-Cena Jul 13, 2021
3d12b30
Merge branch 'master' into navbaritem
Josh-Cena Jul 13, 2021
e9895d6
Merge branch 'master' into navbaritem
slorber Jul 14, 2021
1075477
minor refactor
slorber Jul 14, 2021
07ce433
minor refactors
slorber Jul 14, 2021
4ff53d7
allow usage of ES2019 in browser code
slorber Jul 14, 2021
c12f0ba
revert NavLink rename
slorber Jul 14, 2021
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/* eslint-disable camelcase */

declare module '@docusaurus/plugin-content-docs-types' {
import type {VersionBanner} from './types';
type VersionBanner = import('./types').VersionBanner;
slorber marked this conversation as resolved.
Show resolved Hide resolved

export type PropVersionMetadata = {
pluginId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function UnmaintainedVersionLabel({
}

const BannerLabelComponents: Record<
Props['versionMetadata']['banner'],
Exclude<Props['versionMetadata']['banner'], 'none'>,
ComponentType<BannerLabelComponentProps>
> = {
unreleased: UnreleasedVersionLabel,
Expand Down
23 changes: 11 additions & 12 deletions packages/docusaurus-theme-classic/src/theme/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import useHideableNavbar from '@theme/hooks/useHideableNavbar';
import useLockBodyScroll from '@theme/hooks/useLockBodyScroll';
import useWindowSize from '@theme/hooks/useWindowSize';
import NavbarItem from '@theme/NavbarItem';
import NavbarItem, {Props as NavbarItemConfig} from '@theme/NavbarItem';
import Logo from '@theme/Logo';
import IconMenu from '@theme/IconMenu';

Expand All @@ -28,9 +28,14 @@ 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) {
function splitNavItemsByPosition(items: NavbarItemConfig[]) {
const leftItems = items.filter(
(item) => (item.position ?? DefaultNavItemPosition) === 'left',
);
Expand Down Expand Up @@ -132,9 +137,7 @@ function NavbarMobileSidebar({
toggleSidebar,
}: NavbarMobileSidebarProps) {
useLockBodyScroll(sidebarShown);
const {
navbar: {items},
} = useThemeConfig();
const items = useNavbarItems();

const colorModeToggle = useColorModeToggle();

Expand Down Expand Up @@ -166,12 +169,7 @@ function NavbarMobileSidebar({
<div className="menu">
<ul className="menu__list">
{items.map((item, i) => (
<NavbarItem
mobile
{...(item as any)} // TODO fix typing
onClick={toggleSidebar}
key={i}
/>
<NavbarItem mobile {...item} onClick={toggleSidebar} key={i} />
))}
</ul>
</div>
Expand All @@ -196,14 +194,15 @@ function NavbarMobileSidebar({

function Navbar(): JSX.Element {
const {
navbar: {items, hideOnScroll, style},
navbar: {hideOnScroll, style},
} = useThemeConfig();

const mobileSidebar = useMobileSidebar();
const colorModeToggle = useColorModeToggle();

const {navbarRef, isNavbarVisible} = useHideableNavbar(hideOnScroll);

const items = useNavbarItems();
const hasSearchNavbarItem = items.some((item) => item.type === 'search');
const {leftItems, rightItems} = splitNavItemsByPosition(items);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/

import React, {useState, useRef, useEffect} from 'react';
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import {useLocation} from '@docusaurus/router';
import {
isSamePath,
useCollapsible,
Collapsible,
} from '@docusaurus/theme-common';
import type {
NavLinkProps,
DesktopOrMobileNavBarItemProps,
Expand All @@ -25,7 +19,7 @@ import isInternalUrl from '@docusaurus/isInternalUrl';

const dropdownLinkActiveClass = 'dropdown__link--active';

function NavLink({
export function NavLink({
activeBasePath,
activeBaseRegex,
to,
Expand All @@ -34,7 +28,7 @@ function NavLink({
activeClassName = 'navbar__link--active',
prependBaseUrlToHref,
...props
}: NavLinkProps) {
}: NavLinkProps): JSX.Element {
// TODO all this seems hacky
// {to: 'version'} should probably be forbidden, in favor of {to: '/version'}
const toUrl = useBaseUrl(to);
Expand Down Expand Up @@ -75,159 +69,36 @@ function NavLink({
);
}

function NavItemDesktop({
items,
position,
function DefaultNavbarItemDesktop({
className,
isDropdownItem = false,
...props
}: DesktopOrMobileNavBarItemProps) {
const dropdownRef = useRef<HTMLDivElement>(null);
const dropdownMenuRef = useRef<HTMLUListElement>(null);
const [showDropdown, setShowDropdown] = useState(false);

useEffect(() => {
const handleClickOutside = (event) => {
if (!dropdownRef.current || dropdownRef.current.contains(event.target)) {
return;
}

setShowDropdown(false);
};

document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);

return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
};
}, [dropdownRef]);

const navLinkClassNames = (extraClassName?: string, isDropdownItem = false) =>
clsx(
{
'navbar__item navbar__link': !isDropdownItem,
dropdown__link: isDropdownItem,
},
extraClassName,
);

if (!items) {
return <NavLink className={navLinkClassNames(className)} {...props} />;
}

return (
<div
ref={dropdownRef}
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
'dropdown--left': position === 'left',
'dropdown--right': position === 'right',
'dropdown--show': showDropdown,
})}>
<NavLink
className={navLinkClassNames(className)}
{...props}
onClick={props.to ? undefined : (e) => e.preventDefault()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
setShowDropdown(!showDropdown);
}
}}>
{props.children ?? props.label}
</NavLink>
<ul ref={dropdownMenuRef} className="dropdown__menu">
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
<li key={i}>
<NavLink
onKeyDown={(e) => {
if (i === items.length - 1 && e.key === 'Tab') {
e.preventDefault();

setShowDropdown(false);

const nextNavbarItem = (dropdownRef.current as HTMLElement)
.nextElementSibling;

if (nextNavbarItem) {
(nextNavbarItem as HTMLElement).focus();
}
}
}}
activeClassName={dropdownLinkActiveClass}
className={navLinkClassNames(childItemClassName, true)}
{...childItemProps}
/>
</li>
))}
</ul>
</div>
<NavLink
className={clsx(
isDropdownItem ? 'dropdown__link' : 'navbar__item navbar__link',
className,
)}
{...props}
/>
);
}

function NavItemMobile({
items,
function DefaultNavbarItemMobile({
className,
position: _position, // Need to destructure position from props so that it doesn't get passed on.
isDropdownItem: _isDropdownItem,
...props
}: DesktopOrMobileNavBarItemProps) {
const {pathname} = useLocation();
const {collapsed, toggleCollapsed} = useCollapsible({
initialState: () =>
!items?.some((item) => isSamePath(item.to, pathname)) ?? true,
});

const navLinkClassNames = (extraClassName?: string, isSubList = false) =>
clsx(
'menu__link',
{
'menu__link--sublist': isSubList,
},
extraClassName,
);

if (!items) {
return (
<li className="menu__list-item">
<NavLink className={navLinkClassNames(className)} {...props} />
</li>
);
}

return (
<li
className={clsx('menu__list-item', {
'menu__list-item--collapsed': collapsed,
})}>
<NavLink
role="button"
className={navLinkClassNames(className, true)}
{...props}
onClick={(e) => {
e.preventDefault();
toggleCollapsed();
}}>
{props.children ?? props.label}
</NavLink>

<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
<li className="menu__list-item" key={i}>
<NavLink
activeClassName="menu__link--active"
className={navLinkClassNames(childItemClassName)}
{...childItemProps}
onClick={props.onClick}
/>
</li>
))}
</Collapsible>
<li className="menu__list-item">
<NavLink className={clsx('menu__link', className)} {...props} />
</li>
);
}

function DefaultNavbarItem({mobile = false, ...props}: Props): JSX.Element {
const Comp = mobile ? NavItemMobile : NavItemDesktop;
const Comp = mobile ? DefaultNavbarItemMobile : DefaultNavbarItemDesktop;
return <Comp {...props} />;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,10 @@ import clsx from 'clsx';
import type {Props} from '@theme/NavbarItem/DocNavbarItem';
import {useDocsPreferredVersion} from '@docusaurus/theme-common';
import {uniq} from '@docusaurus/utils-common';
import type {
GlobalDataVersion,
GlobalDataDoc,
} from '@docusaurus/plugin-content-docs-types';
import type {GlobalDataVersion} from '@docusaurus/plugin-content-docs-types';

function getDocInVersions(versions: GlobalDataVersion[], docId: string) {
// vanilla-js flatten, TODO replace soon by ES flat() / flatMap()
const allDocs: GlobalDataDoc[] = [].concat(
Copy link
Collaborator

Choose a reason for hiding this comment

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

not 100% sure but I think we need to keep it for older node/browsers

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think this is taken care of by Babel, as long as the target is set to ES5 or ES6?

Copy link
Collaborator

Choose a reason for hiding this comment

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

yes babel probably does something with browserslist

I think I avoided flatMap in the past due to having to support node10

...versions.map((version) => version.docs),
);

const allDocs = versions.flatMap((version) => version.docs);
const doc = allDocs.find((versionDoc) => versionDoc.id === docId);
if (!doc) {
const docIds = allDocs.map((versionDoc) => versionDoc.id).join('\n- ');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import React from 'react';
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
import {
useVersions,
useLatestVersion,
Expand Down Expand Up @@ -52,20 +53,7 @@ export default function DocsVersionDropdownNavbarItem({
};
});

const items = [
...dropdownItemsBefore,
...versionLinks,
...dropdownItemsAfter,
];

// We don't want to render a version dropdown with 0 or 1 item
// If we build the site with a single docs version (onlyIncludeVersions: ['1.0.0'])
// We'd rather render a button instead of a dropdown
if (items.length <= 1) {
return undefined;
}

return items;
return [...dropdownItemsBefore, ...versionLinks, ...dropdownItemsAfter];
}

const items = getItems();
Expand All @@ -78,8 +66,23 @@ export default function DocsVersionDropdownNavbarItem({
const dropdownTo =
mobile && items ? undefined : getVersionMainDoc(dropdownVersion).path;

// We don't want to render a version dropdown with 0 or 1 item
// If we build the site with a single docs version (onlyIncludeVersions: ['1.0.0'])
// We'd rather render a button instead of a dropdown
if (items.length <= 1) {
return (
<DefaultNavbarItem
{...props}
mobile={mobile}
label={dropdownLabel}
to={dropdownTo}
isActive={dropdownActiveClassDisabled ? () => false : undefined}
/>
);
}

return (
<DefaultNavbarItem
<DropdownNavbarItem
{...props}
mobile={mobile}
label={dropdownLabel}
Expand Down