refactor(v2): improve navbar menu + generated config should be normalized (#3088)

* improve navbar menu

* fix errors due to forcePrependBaseUrl being normalized to true

* fix TS errors
This commit is contained in:
Sébastien Lorber 2020-07-22 11:56:28 +02:00 committed by GitHub
parent 4bc50e4a52
commit 27f384a67c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 96 additions and 36 deletions

View file

@ -82,12 +82,18 @@ function NavItemDesktop({items, position, className, ...props}) {
<NavLink
className={navLinkClassNames(className)}
{...props}
onClick={(e) => e.preventDefault()}
onClick={props.to ? undefined : (e) => e.preventDefault()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
function toggle() {
((e.target as HTMLElement)
.parentNode as HTMLElement).classList.toggle('dropdown--show');
}
if (e.key === 'Enter' && !props.to) {
toggle();
}
if (e.key === 'Tab') {
toggle();
}
}}>
{props.label}
</NavLink>

View file

@ -13,14 +13,16 @@ import {
useActiveDocContext,
} from '@theme/hooks/useDocs';
const versionLabel = (version) =>
version.name === 'next' ? 'Next/Master' : version.name;
const versionLabel = (version, nextVersionLabel) =>
version.name === 'next' ? nextVersionLabel : version.name;
const getVersionMainDoc = (version) =>
version.docs.find((doc) => doc.id === version.mainDocId);
export default function DocsVersionDropdownNavbarItem({
mobile,
docsPluginId,
nextVersionLabel,
...props
}) {
const activeDocContext = useActiveDocContext(docsPluginId);
@ -35,7 +37,7 @@ export default function DocsVersionDropdownNavbarItem({
getVersionMainDoc(version);
return {
isNavLink: true,
label: versionLabel(version),
label: versionLabel(version, nextVersionLabel),
to: versionDoc.path,
isActive: () => version === activeDocContext?.activeVersion,
};
@ -43,11 +45,20 @@ export default function DocsVersionDropdownNavbarItem({
const dropdownVersion = activeDocContext.activeVersion ?? latestVersion;
// Mobile is handled a bit differently
const dropdownLabel = mobile
? 'Versions'
: versionLabel(dropdownVersion, nextVersionLabel);
const dropdownTo = mobile
? undefined
: getVersionMainDoc(dropdownVersion).path;
return (
<DefaultNavbarItem
{...props}
label={versionLabel(dropdownVersion)}
to={getVersionMainDoc(dropdownVersion).path}
mobile={mobile}
label={dropdownLabel}
to={dropdownTo}
items={items}
/>
);

View file

@ -12,16 +12,20 @@ import {useActiveVersion, useLatestVersion} from '@theme/hooks/useDocs';
const getVersionMainDoc = (version) =>
version.docs.find((doc) => doc.id === version.mainDocId);
const versionLabel = (version, nextVersionLabel) =>
version.name === 'next' ? nextVersionLabel : version.name;
export default function DocsVersionNavbarItem({
label: staticLabel,
to: staticTo,
docsPluginId,
nextVersionLabel,
...props
}) {
const activeVersion = useActiveVersion(docsPluginId);
const latestVersion = useLatestVersion(docsPluginId);
const version = activeVersion ?? latestVersion;
const label = staticLabel ?? version.name;
const label = staticLabel ?? versionLabel(version, nextVersionLabel);
const path = staticTo ?? getVersionMainDoc(version).path;
return <DefaultNavbarItem {...props} label={label} to={path} />;
}

View file

@ -13,7 +13,6 @@ const DefaultNavbarItemSchema = Joi.object({
items: Joi.array().optional().items(Joi.link('...')),
to: Joi.string(),
href: Joi.string().uri(),
prependBaseUrlToHref: Joi.bool().default(true),
label: Joi.string(),
position: NavbarItemPosition,
activeBasePath: Joi.string(),
@ -28,12 +27,14 @@ const DocsVersionNavbarItemSchema = Joi.object({
label: Joi.string(),
to: Joi.string(),
docsPluginId: Joi.string(),
nextVersionLabel: Joi.string().default('Next'),
});
const DocsVersionDropdownNavbarItemSchema = Joi.object({
type: Joi.string().equal('docsVersionDropdown').required(),
position: NavbarItemPosition,
docsPluginId: Joi.string(),
nextVersionLabel: Joi.string().default('Next'),
});
// Can this be made easier? :/

View file

@ -22,7 +22,7 @@ interface Props {
readonly isNavLink?: boolean;
readonly to?: string;
readonly activeClassName?: string;
readonly href: string;
readonly href?: string;
readonly children?: ReactNode;
}
@ -89,14 +89,16 @@ function Link({isNavLink, activeClassName, ...props}: Props): JSX.Element {
const isAnchorLink = targetLink?.startsWith('#') ?? false;
const isRegularHtmlLink = !targetLink || !isInternal || isAnchorLink;
if (isInternal && !isAnchorLink) {
if (targetLink && isInternal && !isAnchorLink) {
if (targetLink && targetLink.startsWith('/http')) {
console.log('collectLink', props);
}
linksCollector.collectLink(targetLink);
}
return isRegularHtmlLink ? (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a
// @ts-expect-error: href specified twice needed to pass children and other user specified props
href={targetLink}
{...(!isInternal && {target: '_blank', rel: 'noopener noreferrer'})}
{...props}

View file

@ -8,6 +8,10 @@
import isInternalUrl from '../isInternalUrl';
describe('isInternalUrl', () => {
test('should be true for empty links', () => {
expect(isInternalUrl('')).toBeTruthy();
});
test('should be true for root relative links', () => {
expect(isInternalUrl('/foo/bar')).toBeTruthy();
});
@ -35,4 +39,12 @@ describe('isInternalUrl', () => {
test('should be false for mailto links', () => {
expect(isInternalUrl('mailto:someone@example.com')).toBeFalsy();
});
test('should be false for undefined links', () => {
expect(isInternalUrl(undefined)).toBeFalsy();
});
test('should be true for root relative links', () => {
expect(isInternalUrl('//reactjs.org')).toBeFalsy();
});
});

View file

@ -12,6 +12,8 @@ jest.mock('../useDocusaurusContext', () => jest.fn(), {virtual: true});
const mockedContext = <jest.Mock>useDocusaurusContext;
const forcePrepend = {forcePrependBaseUrl: true};
describe('useBaseUrl', () => {
test('empty base URL', () => {
mockedContext.mockImplementation(() => ({
@ -31,8 +33,9 @@ describe('useBaseUrl', () => {
expect(useBaseUrl('/hello/byebye/')).toEqual('/hello/byebye/');
expect(useBaseUrl('https://github.com')).toEqual('https://github.com');
expect(useBaseUrl('//reactjs.org')).toEqual('//reactjs.org');
expect(useBaseUrl('https://site.com', {forcePrependBaseUrl: true})).toEqual(
'/https://site.com',
expect(useBaseUrl('//reactjs.org', forcePrepend)).toEqual('//reactjs.org');
expect(useBaseUrl('https://site.com', forcePrepend)).toEqual(
'https://site.com',
);
expect(useBaseUrl('/hello/byebye', {absolute: true})).toEqual(
'https://v2.docusaurus.io/hello/byebye',
@ -57,8 +60,9 @@ describe('useBaseUrl', () => {
expect(useBaseUrl('/hello/byebye/')).toEqual('/docusaurus/hello/byebye/');
expect(useBaseUrl('https://github.com')).toEqual('https://github.com');
expect(useBaseUrl('//reactjs.org')).toEqual('//reactjs.org');
expect(useBaseUrl('https://site.com', {forcePrependBaseUrl: true})).toEqual(
'/docusaurus/https://site.com',
expect(useBaseUrl('//reactjs.org', forcePrepend)).toEqual('//reactjs.org');
expect(useBaseUrl('https://site.com', forcePrepend)).toEqual(
'https://site.com',
);
expect(useBaseUrl('/hello/byebye', {absolute: true})).toEqual(
'https://v2.docusaurus.io/docusaurus/hello/byebye',
@ -86,9 +90,10 @@ describe('useBaseUrlUtils().withBaseUrl()', () => {
expect(withBaseUrl('/hello/byebye/')).toEqual('/hello/byebye/');
expect(withBaseUrl('https://github.com')).toEqual('https://github.com');
expect(withBaseUrl('//reactjs.org')).toEqual('//reactjs.org');
expect(
withBaseUrl('https://site.com', {forcePrependBaseUrl: true}),
).toEqual('/https://site.com');
expect(withBaseUrl('//reactjs.org', forcePrepend)).toEqual('//reactjs.org');
expect(withBaseUrl('https://site.com', forcePrepend)).toEqual(
'https://site.com',
);
expect(withBaseUrl('/hello/byebye', {absolute: true})).toEqual(
'https://v2.docusaurus.io/hello/byebye',
);
@ -113,9 +118,10 @@ describe('useBaseUrlUtils().withBaseUrl()', () => {
expect(withBaseUrl('/hello/byebye/')).toEqual('/docusaurus/hello/byebye/');
expect(withBaseUrl('https://github.com')).toEqual('https://github.com');
expect(withBaseUrl('//reactjs.org')).toEqual('//reactjs.org');
expect(
withBaseUrl('https://site.com', {forcePrependBaseUrl: true}),
).toEqual('/docusaurus/https://site.com');
expect(withBaseUrl('//reactjs.org', forcePrepend)).toEqual('//reactjs.org');
expect(withBaseUrl('https://site.com', forcePrepend)).toEqual(
'https://site.com',
);
expect(withBaseUrl('/hello/byebye', {absolute: true})).toEqual(
'https://v2.docusaurus.io/docusaurus/hello/byebye',
);

View file

@ -5,6 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
export default function isInternalUrl(url: string): boolean {
return /^(\w*:|\/\/)/.test(url) === false;
export function hasProtocol(url: string) {
return /^(\w*:|\/\/)/.test(url) === true;
}
export default function isInternalUrl(url?: string): boolean {
return typeof url !== 'undefined' && !hasProtocol(url);
}

View file

@ -9,6 +9,8 @@ import useDocusaurusContext from './useDocusaurusContext';
import isInternalUrl from './isInternalUrl';
type BaseUrlOptions = Partial<{
// note: if the url has a protocol, we never prepend it
// (it never makes any sense to do so)
forcePrependBaseUrl: boolean;
absolute: boolean;
}>;
@ -23,21 +25,21 @@ function addBaseUrl(
return url;
}
if (forcePrependBaseUrl) {
return baseUrl + url;
}
if (!isInternalUrl(url)) {
return url;
}
if (forcePrependBaseUrl) {
return baseUrl + url;
}
const basePath = baseUrl + url.replace(/^\//, '');
return absolute ? siteUrl + basePath : basePath;
}
export type BaseUrlUtils = {
withBaseUrl: (url: string, options: BaseUrlOptions) => string;
withBaseUrl: (url: string, options?: BaseUrlOptions) => string;
};
export function useBaseUrlUtils(): BaseUrlUtils {
@ -53,7 +55,7 @@ export function useBaseUrlUtils(): BaseUrlUtils {
export default function useBaseUrl(
url: string,
options: BaseUrlOptions,
options: BaseUrlOptions = {},
): string {
const {withBaseUrl} = useBaseUrlUtils();
return withBaseUrl(url, options);

View file

@ -71,11 +71,6 @@ export async function load(
// Context.
const context: LoadContext = loadContext(siteDir, customOutDir);
const {generatedFilesDir, siteConfig, outDir, baseUrl} = context;
const genSiteConfig = generate(
generatedFilesDir,
CONFIG_FILE_NAME,
`export default ${JSON.stringify(siteConfig, null, 2)};`,
);
// Plugins.
const pluginConfigs: PluginConfig[] = loadPluginConfigs(context);
@ -84,6 +79,14 @@ export async function load(
context,
});
// Site config must be generated after plugins
// We want the generated config to have been normalized by the plugins!
const genSiteConfig = generate(
generatedFilesDir,
CONFIG_FILE_NAME,
`export default ${JSON.stringify(siteConfig, null, 2)};`,
);
// Themes.
const fallbackTheme = path.resolve(__dirname, '../client/theme-fallback');
const pluginThemes: string[] = plugins

View file

@ -178,6 +178,7 @@ module.exports = {
{
type: 'docsVersionDropdown',
position: 'left',
nextVersionLabel: '2.0.0-next',
},
{to: 'blog', label: 'Blog', position: 'left'},
{to: 'showcase', label: 'Showcase', position: 'left'},
@ -188,8 +189,8 @@ module.exports = {
activeBaseRegex: `docs/next/(support|team|resources)`,
},
{
type: 'docsVersion',
to: 'versions',
label: 'All versions',
position: 'right',
},
{

View file

@ -48,3 +48,11 @@ html[data-theme='dark'] .header-github-link:before {
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")
no-repeat;
}
/*
TODO temporary, should be handled by infima next release
https://github.com/facebookincubator/infima/commit/7820399af53c182b1879aa6d7fceb4d296f78ce0
*/
.navbar__item.dropdown .navbar__link[href] {
pointer-events: all;
}