mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-18 19:46:57 +02:00
refactor(v2): various dropdown improvements (#3585)
* refactor(v2): various dropdown improvements * Remove invalid attr from markup * Better naming * Update types * Fix * Remove attr position correctly * Test * Add test
This commit is contained in:
parent
bfefc46436
commit
8f5c632cdf
5 changed files with 130 additions and 65 deletions
|
@ -34,8 +34,7 @@
|
||||||
"prismjs": "^1.20.0",
|
"prismjs": "^1.20.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-toggle": "^4.1.1",
|
"react-toggle": "^4.1.1"
|
||||||
"use-onclickoutside": "^0.3.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "2.0.0-alpha.65",
|
"@docusaurus/module-type-aliases": "2.0.0-alpha.65",
|
||||||
|
|
|
@ -96,6 +96,89 @@ describe('themeConfig', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should allow possible types of navbar items', () => {
|
||||||
|
const config = {
|
||||||
|
navbar: {
|
||||||
|
items: [
|
||||||
|
// Doc link
|
||||||
|
{
|
||||||
|
type: 'doc',
|
||||||
|
position: 'left',
|
||||||
|
docId: 'intro',
|
||||||
|
label: 'Introduction',
|
||||||
|
activeSidebarClassName: 'custom-class',
|
||||||
|
},
|
||||||
|
// Regular link
|
||||||
|
{
|
||||||
|
to: '/guide/',
|
||||||
|
label: 'Guide',
|
||||||
|
position: 'left',
|
||||||
|
activeBaseRegex: '/guide/',
|
||||||
|
},
|
||||||
|
// Regular dropdown
|
||||||
|
{
|
||||||
|
label: 'Community',
|
||||||
|
position: 'right',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Facebook',
|
||||||
|
href: 'https://.facebook.com/',
|
||||||
|
target: '_self',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'GitHub',
|
||||||
|
href: 'https://github.com/facebook/docusaurus',
|
||||||
|
className: 'github-link',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Doc version dropdown
|
||||||
|
{
|
||||||
|
type: 'docsVersionDropdown',
|
||||||
|
position: 'right',
|
||||||
|
dropdownActiveClassDisabled: true,
|
||||||
|
dropdownItemsBefore: [
|
||||||
|
{
|
||||||
|
href:
|
||||||
|
'https://www.npmjs.com/package/docusaurus?activeTab=versions',
|
||||||
|
label: 'Versions on npm',
|
||||||
|
className: 'npm-styled',
|
||||||
|
target: '_self',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dropdownItemsAfter: [
|
||||||
|
{
|
||||||
|
to: '/versions',
|
||||||
|
label: 'All versions',
|
||||||
|
className: 'all_vers',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// External link with custom data attribute
|
||||||
|
{
|
||||||
|
href: 'https://github.com/facebook/docusaurus',
|
||||||
|
position: 'right',
|
||||||
|
className: 'header-github-link',
|
||||||
|
'aria-label': 'GitHub repository',
|
||||||
|
},
|
||||||
|
// Docs version
|
||||||
|
{
|
||||||
|
type: 'docsVersion',
|
||||||
|
position: 'left',
|
||||||
|
label: 'Current version',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(testValidateThemeConfig(config)).toEqual({
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
navbar: {
|
||||||
|
...DEFAULT_CONFIG.navbar,
|
||||||
|
...config.navbar,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('should allow empty alt tags for the logo image in the header', () => {
|
test('should allow empty alt tags for the logo image in the header', () => {
|
||||||
const altTagConfig = {
|
const altTagConfig = {
|
||||||
navbar: {
|
navbar: {
|
||||||
|
|
|
@ -5,13 +5,12 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useState} from 'react';
|
import React, {useState, useRef, useEffect} from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
import {useLocation} from '@docusaurus/router';
|
import {useLocation} from '@docusaurus/router';
|
||||||
import {isSamePath} from '../../utils';
|
import {isSamePath} from '../../utils';
|
||||||
import useOnClickOutside from 'use-onclickoutside';
|
|
||||||
import type {
|
import type {
|
||||||
NavLinkProps,
|
NavLinkProps,
|
||||||
DesktopOrMobileNavBarItemProps,
|
DesktopOrMobileNavBarItemProps,
|
||||||
|
@ -67,21 +66,28 @@ function NavItemDesktop({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: DesktopOrMobileNavBarItemProps) {
|
}: DesktopOrMobileNavBarItemProps) {
|
||||||
const dropDownRef = React.useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const dropDownMenuRef = React.useRef<HTMLUListElement>(null);
|
const dropdownMenuRef = useRef<HTMLUListElement>(null);
|
||||||
const [showDropDown, setShowDropDown] = useState(false);
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
useOnClickOutside(dropDownRef, () => toggle(false));
|
|
||||||
function toggle(state: boolean) {
|
|
||||||
if (state) {
|
|
||||||
const firstNavLinkOfULElement =
|
|
||||||
dropDownMenuRef?.current?.firstChild?.firstChild;
|
|
||||||
|
|
||||||
if (firstNavLinkOfULElement) {
|
useEffect(() => {
|
||||||
(firstNavLinkOfULElement as HTMLElement).focus();
|
const handleClickOutside = (event) => {
|
||||||
|
if (!dropdownRef.current || dropdownRef.current.contains(event.target)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
setShowDropDown(state);
|
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) =>
|
const navLinkClassNames = (extraClassName?: string, isDropdownItem = false) =>
|
||||||
clsx(
|
clsx(
|
||||||
{
|
{
|
||||||
|
@ -97,11 +103,11 @@ function NavItemDesktop({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={dropDownRef}
|
ref={dropdownRef}
|
||||||
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
|
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
|
||||||
'dropdown--left': position === 'left',
|
'dropdown--left': position === 'left',
|
||||||
'dropdown--right': position === 'right',
|
'dropdown--right': position === 'right',
|
||||||
'dropdown--show': showDropDown,
|
'dropdown--show': showDropdown,
|
||||||
})}>
|
})}>
|
||||||
<NavLink
|
<NavLink
|
||||||
className={navLinkClassNames(className)}
|
className={navLinkClassNames(className)}
|
||||||
|
@ -110,23 +116,23 @@ function NavItemDesktop({
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggle(true);
|
setShowDropdown(!showDropdown);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{props.label}
|
{props.label}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<ul ref={dropDownMenuRef} className="dropdown__menu">
|
<ul ref={dropdownMenuRef} className="dropdown__menu">
|
||||||
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
|
{items.map(({className: childItemClassName, ...childItemProps}, i) => (
|
||||||
<li key={i}>
|
<li key={i}>
|
||||||
<NavLink
|
<NavLink
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (i === items.length - 1 && e.key === 'Tab') {
|
if (i === items.length - 1 && e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggle(false);
|
|
||||||
|
|
||||||
const nextNavbarItem =
|
setShowDropdown(false);
|
||||||
dropDownRef.current &&
|
|
||||||
(dropDownRef.current as HTMLElement).nextElementSibling;
|
const nextNavbarItem = (dropdownRef.current as HTMLElement)
|
||||||
|
.nextElementSibling;
|
||||||
|
|
||||||
if (nextNavbarItem) {
|
if (nextNavbarItem) {
|
||||||
(nextNavbarItem as HTMLElement).focus();
|
(nextNavbarItem as HTMLElement).focus();
|
||||||
|
@ -146,8 +152,8 @@ function NavItemDesktop({
|
||||||
|
|
||||||
function NavItemMobile({
|
function NavItemMobile({
|
||||||
items,
|
items,
|
||||||
position: _position,
|
|
||||||
className,
|
className,
|
||||||
|
position: _position, // Need to destructure position from props so that it doesn't get passed on.
|
||||||
...props
|
...props
|
||||||
}: DesktopOrMobileNavBarItemProps) {
|
}: DesktopOrMobileNavBarItemProps) {
|
||||||
const {pathname} = useLocation();
|
const {pathname} = useLocation();
|
||||||
|
@ -155,7 +161,6 @@ function NavItemMobile({
|
||||||
() => !items?.some((item) => isSamePath(item.to, pathname)) ?? true,
|
() => !items?.some((item) => isSamePath(item.to, pathname)) ?? true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Need to destructure position from props so that it doesn't get passed on.
|
|
||||||
const navLinkClassNames = (extraClassName?: string, isSubList = false) =>
|
const navLinkClassNames = (extraClassName?: string, isSubList = false) =>
|
||||||
clsx(
|
clsx(
|
||||||
'menu__link',
|
'menu__link',
|
||||||
|
|
|
@ -45,23 +45,26 @@ exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
||||||
|
|
||||||
const NavbarItemPosition = Joi.string().equal('left', 'right').default('left');
|
const NavbarItemPosition = Joi.string().equal('left', 'right').default('left');
|
||||||
|
|
||||||
// TODO we should probably create a custom navbar item type "dropdown"
|
const BaseNavbarItemSchema = Joi.object({
|
||||||
// having this recursive structure is bad because we only support 2 levels
|
|
||||||
// + parent/child don't have exactly the same props
|
|
||||||
const DefaultNavbarItemSchema = Joi.object({
|
|
||||||
items: Joi.array().optional().items(Joi.link('...')),
|
|
||||||
to: Joi.string(),
|
to: Joi.string(),
|
||||||
href: URISchema,
|
href: URISchema,
|
||||||
label: Joi.string(),
|
label: Joi.string(),
|
||||||
position: NavbarItemPosition,
|
|
||||||
activeBasePath: Joi.string(),
|
|
||||||
activeBaseRegex: Joi.string(),
|
|
||||||
className: Joi.string(),
|
className: Joi.string(),
|
||||||
'aria-label': Joi.string(),
|
prependBaseUrlToHref: Joi.string(),
|
||||||
})
|
})
|
||||||
// We allow any unknown attributes on the links
|
// We allow any unknown attributes on the links
|
||||||
// (users may need additional attributes like target, aria-role, data-customAttribute...)
|
// (users may need additional attributes like target, aria-role, data-customAttribute...)
|
||||||
.unknown();
|
.unknown();
|
||||||
|
|
||||||
|
// TODO we should probably create a custom navbar item type "dropdown"
|
||||||
|
// having this recursive structure is bad because we only support 2 levels
|
||||||
|
// + parent/child don't have exactly the same props
|
||||||
|
const DefaultNavbarItemSchema = BaseNavbarItemSchema.append({
|
||||||
|
items: Joi.array().optional().items(BaseNavbarItemSchema),
|
||||||
|
position: NavbarItemPosition,
|
||||||
|
activeBasePath: Joi.string(),
|
||||||
|
activeBaseRegex: Joi.string(),
|
||||||
|
});
|
||||||
// TODO the dropdown parent item can have no href/to
|
// TODO the dropdown parent item can have no href/to
|
||||||
// should check should not apply to dropdown parent item
|
// should check should not apply to dropdown parent item
|
||||||
// .xor('href', 'to');
|
// .xor('href', 'to');
|
||||||
|
@ -79,8 +82,8 @@ const DocsVersionDropdownNavbarItemSchema = Joi.object({
|
||||||
position: NavbarItemPosition,
|
position: NavbarItemPosition,
|
||||||
docsPluginId: Joi.string(),
|
docsPluginId: Joi.string(),
|
||||||
dropdownActiveClassDisabled: Joi.boolean(),
|
dropdownActiveClassDisabled: Joi.boolean(),
|
||||||
dropdownItemsBefore: Joi.array().items(DefaultNavbarItemSchema).default([]),
|
dropdownItemsBefore: Joi.array().items(BaseNavbarItemSchema).default([]),
|
||||||
dropdownItemsAfter: Joi.array().items(DefaultNavbarItemSchema).default([]),
|
dropdownItemsAfter: Joi.array().items(BaseNavbarItemSchema).default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const DocItemSchema = Joi.object({
|
const DocItemSchema = Joi.object({
|
||||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -4992,11 +4992,6 @@ archiver@^4.0.0:
|
||||||
tar-stream "^2.1.2"
|
tar-stream "^2.1.2"
|
||||||
zip-stream "^3.0.1"
|
zip-stream "^3.0.1"
|
||||||
|
|
||||||
are-passive-events-supported@^1.1.0:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/are-passive-events-supported/-/are-passive-events-supported-1.1.1.tgz#3db180a1753a2186a2de50a32cded3ac0979f5dc"
|
|
||||||
integrity sha512-5wnvlvB/dTbfrCvJ027Y4L4gW/6Mwoy1uFSavney0YO++GU+0e/flnjiBBwH+1kh7xNCgCOGvmJC3s32joYbww==
|
|
||||||
|
|
||||||
are-we-there-yet@~1.1.2:
|
are-we-there-yet@~1.1.2:
|
||||||
version "1.1.5"
|
version "1.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
|
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
|
||||||
|
@ -21175,26 +21170,6 @@ url@^0.11.0:
|
||||||
punycode "1.3.2"
|
punycode "1.3.2"
|
||||||
querystring "0.2.0"
|
querystring "0.2.0"
|
||||||
|
|
||||||
use-isomorphic-layout-effect@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.0.0.tgz#f56b4ed633e1c21cd9fc76fe249002a1c28989fb"
|
|
||||||
integrity sha512-JMwJ7Vd86NwAt1jH7q+OIozZSIxA4ND0fx6AsOe2q1H8ooBUp5aN6DvVCqZiIaYU6JaMRJGyR0FO7EBCIsb/Rg==
|
|
||||||
|
|
||||||
use-latest@^1.0.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.1.0.tgz#7bf9684555869c3f5f37e10d0884c8accf4d3aa6"
|
|
||||||
integrity sha512-gF04d0ZMV3AMB8Q7HtfkAWe+oq1tFXP6dZKwBHQF5nVXtGsh2oAYeeqma5ZzxtlpOcW8Ro/tLcfmEodjDeqtuw==
|
|
||||||
dependencies:
|
|
||||||
use-isomorphic-layout-effect "^1.0.0"
|
|
||||||
|
|
||||||
use-onclickoutside@^0.3.1:
|
|
||||||
version "0.3.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/use-onclickoutside/-/use-onclickoutside-0.3.1.tgz#fdd723a6a499046b6bc761e4a03af432eee5917b"
|
|
||||||
integrity sha512-aahvbW5+G0XJfzj31FJeLsvc6qdKbzeTsQ8EtkHHq5qTg6bm/qkJeKLcgrpnYeHDDbd7uyhImLGdkbM9BRzOHQ==
|
|
||||||
dependencies:
|
|
||||||
are-passive-events-supported "^1.1.0"
|
|
||||||
use-latest "^1.0.0"
|
|
||||||
|
|
||||||
use@^3.1.0:
|
use@^3.1.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue