mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-03 11:22:30 +02:00
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:
parent
4bc50e4a52
commit
27f384a67c
12 changed files with 96 additions and 36 deletions
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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? :/
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue