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