mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-04 01:09:20 +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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue