mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-06 10:20:09 +02:00
fix(core): revert wrong anchor link implementation change (#10311)
This commit is contained in:
parent
61d6858864
commit
868d72fe4f
3 changed files with 371 additions and 6 deletions
10
.github/workflows/build-perf.yml
vendored
10
.github/workflows/build-perf.yml
vendored
|
@ -46,9 +46,11 @@ jobs:
|
||||||
uses: preactjs/compressed-size-action@f780fd104362cfce9e118f9198df2ee37d12946c # v2
|
uses: preactjs/compressed-size-action@f780fd104362cfce9e118f9198df2ee37d12946c # v2
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build-script: build:website:en
|
build-script: build:website:fast
|
||||||
clean-script: clear:website # see https://github.com/facebook/docusaurus/pull/6838
|
clean-script: clear:website # see https://github.com/facebook/docusaurus/pull/6838
|
||||||
pattern: '{website/build/assets/js/main*js,website/build/assets/css/styles*css,website/.docusaurus/globalData.json,website/.docusaurus/registry.js,website/.docusaurus/routes.js,website/.docusaurus/routesChunkNames.json,website/.docusaurus/site-metadata.json,website/.docusaurus/codeTranslations.json,website/.docusaurus/i18n.json,website/.docusaurus/docusaurus.config.mjs,website/build/index.html,website/build/blog/index.html,website/build/blog/**/introducing-docusaurus/*,website/build/docs/index.html,website/build/docs/installation/index.html,website/build/tests/docs/index.html,website/build/tests/docs/standalone/index.html}'
|
pattern: '{website/build/assets/js/main*js,website/build/assets/css/styles*css,website/.docusaurus/globalData.json,website/.docusaurus/registry.js,website/.docusaurus/routes.js,website/.docusaurus/routesChunkNames.json,website/.docusaurus/site-metadata.json,website/.docusaurus/codeTranslations.json,website/.docusaurus/i18n.json,website/.docusaurus/docusaurus.config.mjs,website/build/index.html,website/build/docs.html,website/build/docs/**/*.html,website/build/blog.html,website/build/blog/**/*.html}'
|
||||||
|
# HTML files: exclude versioned docs pages, tags pages, html redirect files
|
||||||
|
exclude: '{website/build/docs/?.?.?/**/*.html,website/build/docs/next/**/*.html,website/build/blog/tags/**/*.html,**/*.html.html}'
|
||||||
strip-hash: '\.([^;]\w{7})\.'
|
strip-hash: '\.([^;]\w{7})\.'
|
||||||
minimum-change-threshold: 30
|
minimum-change-threshold: 30
|
||||||
compression: none
|
compression: none
|
||||||
|
@ -68,11 +70,11 @@ jobs:
|
||||||
|
|
||||||
# Ensure build with a cold cache does not increase too much
|
# Ensure build with a cold cache does not increase too much
|
||||||
- name: Build (cold cache)
|
- name: Build (cold cache)
|
||||||
run: yarn workspace website build --locale en
|
run: yarn build:website:fast
|
||||||
timeout-minutes: 8
|
timeout-minutes: 8
|
||||||
|
|
||||||
# Ensure build with a warm cache does not increase too much
|
# Ensure build with a warm cache does not increase too much
|
||||||
- name: Build (warm cache)
|
- name: Build (warm cache)
|
||||||
run: yarn workspace website build --locale en
|
run: yarn build:website:fast
|
||||||
timeout-minutes: 2
|
timeout-minutes: 2
|
||||||
# TODO post a GitHub comment with build with perf warnings?
|
# TODO post a GitHub comment with build with perf warnings?
|
||||||
|
|
|
@ -135,7 +135,7 @@ function Link(
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If IO is not supported. We prefetch by default (only once).
|
// If IO is not supported. We prefetch by default (only once).
|
||||||
if (!IOSupported && isInternal) {
|
if (!IOSupported && isInternal && ExecutionEnvironment.canUseDOM) {
|
||||||
if (targetLink != null) {
|
if (targetLink != null) {
|
||||||
window.docusaurus.prefetch(targetLink);
|
window.docusaurus.prefetch(targetLink);
|
||||||
}
|
}
|
||||||
|
@ -157,7 +157,15 @@ function Link(
|
||||||
const hasInternalTarget = !props.target || props.target === '_self';
|
const hasInternalTarget = !props.target || props.target === '_self';
|
||||||
|
|
||||||
// Should we use a regular <a> tag instead of React-Router Link component?
|
// Should we use a regular <a> tag instead of React-Router Link component?
|
||||||
const isRegularHtmlLink = !targetLink || !isInternal || !hasInternalTarget;
|
const isRegularHtmlLink =
|
||||||
|
!targetLink ||
|
||||||
|
!isInternal ||
|
||||||
|
!hasInternalTarget ||
|
||||||
|
// When using the hash router, we can't use the regular <a> link for anchors
|
||||||
|
// We need to use React Router to navigate to /#/pathname/#anchor
|
||||||
|
// And not /#anchor
|
||||||
|
// See also https://github.com/facebook/docusaurus/pull/10311
|
||||||
|
(isAnchorLink && router !== 'hash');
|
||||||
|
|
||||||
if (!noBrokenLinkCheck && (isAnchorLink || !isRegularHtmlLink)) {
|
if (!noBrokenLinkCheck && (isAnchorLink || !isRegularHtmlLink)) {
|
||||||
brokenLinks.collectLink(targetLink!);
|
brokenLinks.collectLink(targetLink!);
|
||||||
|
@ -167,6 +175,12 @@ function Link(
|
||||||
brokenLinks.collectAnchor(props.id);
|
brokenLinks.collectAnchor(props.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These props are only added in unit tests to assert/capture the type of link
|
||||||
|
const testOnlyProps =
|
||||||
|
process.env.NODE_ENV === 'test'
|
||||||
|
? {'data-test-link-type': isRegularHtmlLink ? 'regular' : 'react-router'}
|
||||||
|
: {};
|
||||||
|
|
||||||
return isRegularHtmlLink ? (
|
return isRegularHtmlLink ? (
|
||||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, @docusaurus/no-html-links
|
// eslint-disable-next-line jsx-a11y/anchor-has-content, @docusaurus/no-html-links
|
||||||
<a
|
<a
|
||||||
|
@ -175,6 +189,7 @@ function Link(
|
||||||
{...(targetLinkUnprefixed &&
|
{...(targetLinkUnprefixed &&
|
||||||
!isInternal && {target: '_blank', rel: 'noopener noreferrer'})}
|
!isInternal && {target: '_blank', rel: 'noopener noreferrer'})}
|
||||||
{...props}
|
{...props}
|
||||||
|
{...testOnlyProps}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LinkComponent
|
<LinkComponent
|
||||||
|
@ -186,6 +201,7 @@ function Link(
|
||||||
// Avoid "React does not recognize the `activeClassName` prop on a DOM
|
// Avoid "React does not recognize the `activeClassName` prop on a DOM
|
||||||
// element"
|
// element"
|
||||||
{...(isNavLink && {isActive, activeClassName})}
|
{...(isNavLink && {isActive, activeClassName})}
|
||||||
|
{...testOnlyProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
347
packages/docusaurus/src/client/exports/__tests__/Link.test.tsx
Normal file
347
packages/docusaurus/src/client/exports/__tests__/Link.test.tsx
Normal file
|
@ -0,0 +1,347 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
|
||||||
|
import React, {type ReactNode} from 'react';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
import {fromPartial} from '@total-typescript/shoehorn';
|
||||||
|
import {StaticRouter} from 'react-router-dom';
|
||||||
|
import Link from '../Link';
|
||||||
|
import {Context} from '../../docusaurusContext';
|
||||||
|
import type {DocusaurusContext} from '@docusaurus/types';
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
trailingSlash: boolean | undefined;
|
||||||
|
baseUrl: string;
|
||||||
|
router: DocusaurusContext['siteConfig']['future']['experimental_router'];
|
||||||
|
currentLocation: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOptions: Options = {
|
||||||
|
trailingSlash: undefined,
|
||||||
|
baseUrl: '/',
|
||||||
|
router: 'browser',
|
||||||
|
// currentLocation is nested on purpose, shows relative link resolution
|
||||||
|
currentLocation: '/sub/category/currentPathname',
|
||||||
|
};
|
||||||
|
|
||||||
|
function createDocusaurusContext(
|
||||||
|
partialOptions: Partial<Options>,
|
||||||
|
): DocusaurusContext {
|
||||||
|
const options: Options = {...defaultOptions, ...partialOptions};
|
||||||
|
return fromPartial({
|
||||||
|
siteConfig: {
|
||||||
|
baseUrl: options.baseUrl,
|
||||||
|
trailingSlash: options.trailingSlash,
|
||||||
|
future: {
|
||||||
|
experimental_router: options.router,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLinkRenderer(defaultRendererOptions: Partial<Options> = {}) {
|
||||||
|
return (linkJsx: ReactNode, testOptions: Partial<Options> = {}) => {
|
||||||
|
const options: Options = {
|
||||||
|
...defaultOptions,
|
||||||
|
...defaultRendererOptions,
|
||||||
|
...testOptions,
|
||||||
|
};
|
||||||
|
const docusaurusContext = createDocusaurusContext(options);
|
||||||
|
return renderer
|
||||||
|
.create(
|
||||||
|
<StaticRouter location={options.currentLocation} context={{}}>
|
||||||
|
<Context.Provider value={docusaurusContext}>
|
||||||
|
{linkJsx}
|
||||||
|
</Context.Provider>
|
||||||
|
</StaticRouter>,
|
||||||
|
)
|
||||||
|
.toJSON();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('<Link>', () => {
|
||||||
|
describe('using "browser" router', () => {
|
||||||
|
const render = createLinkRenderer({router: 'browser'});
|
||||||
|
|
||||||
|
it("can render '/docs/intro'", () => {
|
||||||
|
expect(render(<Link to="/docs/intro" />)).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/docs/intro"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render '/docs/intro' with baseUrl /baseUrl/", () => {
|
||||||
|
expect(render(<Link to="/docs/intro" />, {baseUrl: '/baseUrl/'}))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/baseUrl/docs/intro"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render '/docs/intro' with baseUrl /docs/", () => {
|
||||||
|
// TODO Docusaurus v4 ?
|
||||||
|
// Change weird historical baseUrl behavior
|
||||||
|
// we should link to /docs/docs/intro, not /docs/intro
|
||||||
|
// see https://github.com/facebook/docusaurus/issues/6294
|
||||||
|
expect(render(<Link to="/docs/intro" />, {baseUrl: '/docs/'}))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/docs/intro"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render '/docs/intro' with trailingSlash true", () => {
|
||||||
|
expect(render(<Link to="/docs/intro" />, {trailingSlash: true}))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/docs/intro/"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render '/docs/intro/' with trailingSlash false", () => {
|
||||||
|
expect(render(<Link to="/docs/intro/" />, {trailingSlash: false}))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/docs/intro"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render '#anchor'", () => {
|
||||||
|
expect(render(<Link to="#anchor" />)).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="regular"
|
||||||
|
href="#anchor"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render '/docs/intro#anchor'", () => {
|
||||||
|
expect(render(<Link to="/docs/intro#anchor" />)).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/docs/intro#anchor"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render '/docs/intro/#anchor'", () => {
|
||||||
|
expect(render(<Link to="/docs/intro/#anchor" />)).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/docs/intro/#anchor"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render '/pathname?qs#anchor'", () => {
|
||||||
|
expect(render(<Link to="/pathname?qs#anchor" />)).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/pathname?qs#anchor"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render ''", () => {
|
||||||
|
expect(render(<Link to="" />)).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="regular"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render 'relativeDoc'", () => {
|
||||||
|
expect(render(<Link to="relativeDoc" />)).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/sub/category/relativeDoc"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render './relativeDoc'", () => {
|
||||||
|
expect(render(<Link to="./relativeDoc" />)).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/sub/category/relativeDoc"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render './../relativeDoc?qs#anchor'", () => {
|
||||||
|
expect(render(<Link to="./../relativeDoc?qs#anchor" />))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/sub/relativeDoc?qs#anchor"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render 'https://example.com/xyz'", () => {
|
||||||
|
expect(render(<Link to="https://example.com/xyz" />))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="regular"
|
||||||
|
href="https://example.com/xyz"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render 'pathname:///docs/intro'", () => {
|
||||||
|
expect(render(<Link to="pathname:///docs/intro" />))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="regular"
|
||||||
|
href="/docs/intro"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render 'pathname://docs/intro'", () => {
|
||||||
|
expect(render(<Link to="pathname://docs/intro" />))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="regular"
|
||||||
|
href="docs/intro"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render 'pathname:///docs/intro' with baseUrl /baseUrl/", () => {
|
||||||
|
expect(
|
||||||
|
render(<Link to="pathname:///docs/intro" />, {baseUrl: '/baseUrl/'}),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="regular"
|
||||||
|
href="/baseUrl/docs/intro"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render 'pathname:///docs/intro' with target _self", () => {
|
||||||
|
expect(render(<Link to="pathname:///docs/intro" target="_self" />))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="regular"
|
||||||
|
href="/docs/intro"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_self"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render 'pathname:///docs/intro with trailingSlash: true", () => {
|
||||||
|
expect(
|
||||||
|
render(<Link to="pathname:///docs/intro" />, {trailingSlash: true}),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="regular"
|
||||||
|
href="/docs/intro"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('using "hash" router', () => {
|
||||||
|
const render = createLinkRenderer({router: 'hash'});
|
||||||
|
|
||||||
|
it("can render '/docs/intro'", () => {
|
||||||
|
expect(render(<Link to="/docs/intro" />)).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/docs/intro"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render '#anchor'", () => {
|
||||||
|
// It's important to use React Router link for hash router anchors
|
||||||
|
// See https://github.com/facebook/docusaurus/pull/10311
|
||||||
|
expect(render(<Link to="#anchor" />)).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/sub/category/currentPathname#anchor"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can render './relativeDoc'", () => {
|
||||||
|
// Not sure to remember exactly what's this edge case about
|
||||||
|
// still worth it to capture behavior in tests
|
||||||
|
expect(render(<Link to="./relativeDoc" />)).toMatchInlineSnapshot(`
|
||||||
|
<a
|
||||||
|
data-test-link-type="react-router"
|
||||||
|
href="/relativeDoc"
|
||||||
|
onClick={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
/>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue