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:
Alexey Pyltsyn 2020-10-16 17:41:30 +03:00 committed by GitHub
parent bfefc46436
commit 8f5c632cdf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 130 additions and 65 deletions

View file

@ -34,8 +34,7 @@
"prismjs": "^1.20.0",
"prop-types": "^15.7.2",
"react-router-dom": "^5.1.2",
"react-toggle": "^4.1.1",
"use-onclickoutside": "^0.3.1"
"react-toggle": "^4.1.1"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-alpha.65",

View file

@ -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', () => {
const altTagConfig = {
navbar: {

View file

@ -5,13 +5,12 @@
* 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 Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import {useLocation} from '@docusaurus/router';
import {isSamePath} from '../../utils';
import useOnClickOutside from 'use-onclickoutside';
import type {
NavLinkProps,
DesktopOrMobileNavBarItemProps,
@ -67,21 +66,28 @@ function NavItemDesktop({
className,
...props
}: DesktopOrMobileNavBarItemProps) {
const dropDownRef = React.useRef<HTMLDivElement>(null);
const dropDownMenuRef = React.useRef<HTMLUListElement>(null);
const [showDropDown, setShowDropDown] = useState(false);
useOnClickOutside(dropDownRef, () => toggle(false));
function toggle(state: boolean) {
if (state) {
const firstNavLinkOfULElement =
dropDownMenuRef?.current?.firstChild?.firstChild;
const dropdownRef = useRef<HTMLDivElement>(null);
const dropdownMenuRef = useRef<HTMLUListElement>(null);
const [showDropdown, setShowDropdown] = useState(false);
if (firstNavLinkOfULElement) {
(firstNavLinkOfULElement as HTMLElement).focus();
}
}
setShowDropDown(state);
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(
{
@ -97,11 +103,11 @@ function NavItemDesktop({
return (
<div
ref={dropDownRef}
ref={dropdownRef}
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
'dropdown--left': position === 'left',
'dropdown--right': position === 'right',
'dropdown--show': showDropDown,
'dropdown--show': showDropdown,
})}>
<NavLink
className={navLinkClassNames(className)}
@ -110,23 +116,23 @@ function NavItemDesktop({
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
toggle(true);
setShowDropdown(!showDropdown);
}
}}>
{props.label}
</NavLink>
<ul ref={dropDownMenuRef} className="dropdown__menu">
<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();
toggle(false);
const nextNavbarItem =
dropDownRef.current &&
(dropDownRef.current as HTMLElement).nextElementSibling;
setShowDropdown(false);
const nextNavbarItem = (dropdownRef.current as HTMLElement)
.nextElementSibling;
if (nextNavbarItem) {
(nextNavbarItem as HTMLElement).focus();
@ -146,8 +152,8 @@ function NavItemDesktop({
function NavItemMobile({
items,
position: _position,
className,
position: _position, // Need to destructure position from props so that it doesn't get passed on.
...props
}: DesktopOrMobileNavBarItemProps) {
const {pathname} = useLocation();
@ -155,7 +161,6 @@ function NavItemMobile({
() => !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) =>
clsx(
'menu__link',

View file

@ -45,23 +45,26 @@ exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
const NavbarItemPosition = Joi.string().equal('left', 'right').default('left');
// 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 = Joi.object({
items: Joi.array().optional().items(Joi.link('...')),
const BaseNavbarItemSchema = Joi.object({
to: Joi.string(),
href: URISchema,
label: Joi.string(),
position: NavbarItemPosition,
activeBasePath: Joi.string(),
activeBaseRegex: Joi.string(),
className: Joi.string(),
'aria-label': Joi.string(),
prependBaseUrlToHref: Joi.string(),
})
// We allow any unknown attributes on the links
// (users may need additional attributes like target, aria-role, data-customAttribute...)
.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
// should check should not apply to dropdown parent item
// .xor('href', 'to');
@ -79,8 +82,8 @@ const DocsVersionDropdownNavbarItemSchema = Joi.object({
position: NavbarItemPosition,
docsPluginId: Joi.string(),
dropdownActiveClassDisabled: Joi.boolean(),
dropdownItemsBefore: Joi.array().items(DefaultNavbarItemSchema).default([]),
dropdownItemsAfter: Joi.array().items(DefaultNavbarItemSchema).default([]),
dropdownItemsBefore: Joi.array().items(BaseNavbarItemSchema).default([]),
dropdownItemsAfter: Joi.array().items(BaseNavbarItemSchema).default([]),
});
const DocItemSchema = Joi.object({

View file

@ -4992,11 +4992,6 @@ archiver@^4.0.0:
tar-stream "^2.1.2"
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:
version "1.1.5"
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"
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:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"