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", "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",

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', () => { test('should allow empty alt tags for the logo image in the header', () => {
const altTagConfig = { const altTagConfig = {
navbar: { navbar: {

View file

@ -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,27 +116,27 @@ 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);
setShowDropdown(false);
const nextNavbarItem =
dropDownRef.current && const nextNavbarItem = (dropdownRef.current as HTMLElement)
(dropDownRef.current as HTMLElement).nextElementSibling; .nextElementSibling;
if (nextNavbarItem) { if (nextNavbarItem) {
(nextNavbarItem as HTMLElement).focus(); (nextNavbarItem as HTMLElement).focus();
} }
} }
}} }}
activeClassName="dropdown__link--active" activeClassName="dropdown__link--active"
@ -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',

View file

@ -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({

View file

@ -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"