mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-16 17:52:29 +02:00
fix(theme-classic): fix SkipToContent without JS , refactor, make it public theming API (#8204)
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
22c90cb7c7
commit
aa4fa66794
7 changed files with 124 additions and 78 deletions
|
@ -377,6 +377,14 @@ export default function getSwizzleConfig(): SwizzleConfig {
|
||||||
description:
|
description:
|
||||||
'The search bar component of your site, appearing in the navbar.',
|
'The search bar component of your site, appearing in the navbar.',
|
||||||
},
|
},
|
||||||
|
SkipToContent: {
|
||||||
|
actions: {
|
||||||
|
eject: 'safe',
|
||||||
|
wrap: 'safe',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'The component responsible for implementing the accessibility "skip to content" link (https://www.w3.org/TR/WCAG20-TECHS/G1.html)',
|
||||||
|
},
|
||||||
'prism-include-languages': {
|
'prism-include-languages': {
|
||||||
actions: {
|
actions: {
|
||||||
eject: 'safe',
|
eject: 'safe',
|
||||||
|
|
|
@ -8,7 +8,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
||||||
import {PageMetadata, ThemeClassNames} from '@docusaurus/theme-common';
|
import {
|
||||||
|
PageMetadata,
|
||||||
|
SkipToContentFallbackId,
|
||||||
|
ThemeClassNames,
|
||||||
|
} from '@docusaurus/theme-common';
|
||||||
import {useKeyboardNavigation} from '@docusaurus/theme-common/internal';
|
import {useKeyboardNavigation} from '@docusaurus/theme-common/internal';
|
||||||
import SkipToContent from '@theme/SkipToContent';
|
import SkipToContent from '@theme/SkipToContent';
|
||||||
import AnnouncementBar from '@theme/AnnouncementBar';
|
import AnnouncementBar from '@theme/AnnouncementBar';
|
||||||
|
@ -42,6 +46,7 @@ export default function Layout(props: Props): JSX.Element {
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
id={SkipToContentFallbackId}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
ThemeClassNames.wrapper.main,
|
ThemeClassNames.wrapper.main,
|
||||||
styles.mainWrapper,
|
styles.mainWrapper,
|
||||||
|
|
|
@ -6,26 +6,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Translate, {translate} from '@docusaurus/Translate';
|
import {SkipToContentLink} from '@docusaurus/theme-common';
|
||||||
import {useSkipToContent} from '@docusaurus/theme-common/internal';
|
|
||||||
|
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
export default function SkipToContent(): JSX.Element {
|
export default function SkipToContent(): JSX.Element {
|
||||||
const {containerRef, handleSkip} = useSkipToContent();
|
return <SkipToContentLink className={styles.skipToContent} />;
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
role="region"
|
|
||||||
aria-label={translate({id: 'theme.common.skipToMainContent'})}>
|
|
||||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
|
||||||
<a href="#" className={styles.skipToContent} onClick={handleSkip}>
|
|
||||||
<Translate
|
|
||||||
id="theme.common.skipToMainContent"
|
|
||||||
description="The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation">
|
|
||||||
Skip to main content
|
|
||||||
</Translate>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type React from 'react';
|
|
||||||
import {useCallback, useRef} from 'react';
|
|
||||||
import {useHistory} from '@docusaurus/router';
|
|
||||||
import {useLocationChange} from '../utils/useLocationChange';
|
|
||||||
import {ThemeClassNames} from '../utils/ThemeClassNames';
|
|
||||||
|
|
||||||
function programmaticFocus(el: HTMLElement) {
|
|
||||||
el.setAttribute('tabindex', '-1');
|
|
||||||
el.focus();
|
|
||||||
el.removeAttribute('tabindex');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** This hook wires the logic for a skip-to-content link. */
|
|
||||||
export function useSkipToContent(): {
|
|
||||||
/**
|
|
||||||
* The ref to the container. On page transition, the container will be focused
|
|
||||||
* so that keyboard navigators can instantly interact with the link and jump
|
|
||||||
* to content. **Note:** the type is `RefObject<HTMLDivElement>` only because
|
|
||||||
* the typing for refs don't reflect that the `ref` prop is contravariant, so
|
|
||||||
* using `HTMLElement` causes type-checking to fail. You can plug the ref into
|
|
||||||
* any HTML element, as long as it can be focused.
|
|
||||||
*/
|
|
||||||
containerRef: React.RefObject<HTMLDivElement>;
|
|
||||||
/**
|
|
||||||
* Callback fired when the skip to content link has been interacted with. It
|
|
||||||
* will programmatically focus the main content.
|
|
||||||
*/
|
|
||||||
handleSkip: (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
|
||||||
} {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const {action} = useHistory();
|
|
||||||
const handleSkip = useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const targetElement: HTMLElement | null =
|
|
||||||
document.querySelector('main:first-of-type') ??
|
|
||||||
document.querySelector(`.${ThemeClassNames.wrapper.main}`);
|
|
||||||
|
|
||||||
if (targetElement) {
|
|
||||||
programmaticFocus(targetElement);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useLocationChange(({location}) => {
|
|
||||||
if (containerRef.current && !location.hash && action === 'PUSH') {
|
|
||||||
programmaticFocus(containerRef.current);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {containerRef, handleSkip};
|
|
||||||
}
|
|
|
@ -81,4 +81,9 @@ export {useDocsPreferredVersion} from './contexts/docsPreferredVersion';
|
||||||
|
|
||||||
export {processAdmonitionProps} from './utils/admonitionUtils';
|
export {processAdmonitionProps} from './utils/admonitionUtils';
|
||||||
|
|
||||||
|
export {
|
||||||
|
SkipToContentFallbackId,
|
||||||
|
SkipToContentLink,
|
||||||
|
} from './utils/skipToContentUtils';
|
||||||
|
|
||||||
export {ErrorBoundaryTryAgainButton} from './utils/errorBoundaryUtils';
|
export {ErrorBoundaryTryAgainButton} from './utils/errorBoundaryUtils';
|
||||||
|
|
|
@ -117,6 +117,5 @@ export {
|
||||||
export {useLockBodyScroll} from './hooks/useLockBodyScroll';
|
export {useLockBodyScroll} from './hooks/useLockBodyScroll';
|
||||||
export {useSearchPage} from './hooks/useSearchPage';
|
export {useSearchPage} from './hooks/useSearchPage';
|
||||||
export {useCodeWordWrap} from './hooks/useCodeWordWrap';
|
export {useCodeWordWrap} from './hooks/useCodeWordWrap';
|
||||||
export {useSkipToContent} from './hooks/useSkipToContent';
|
|
||||||
export {getPrismCssVariables} from './utils/codeBlockUtils';
|
export {getPrismCssVariables} from './utils/codeBlockUtils';
|
||||||
export {useBackToTopButton} from './hooks/useBackToTopButton';
|
export {useBackToTopButton} from './hooks/useBackToTopButton';
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useCallback, useRef, type ComponentProps} from 'react';
|
||||||
|
import {useHistory} from '@docusaurus/router';
|
||||||
|
import {translate} from '@docusaurus/Translate';
|
||||||
|
import {useLocationChange} from './useLocationChange';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the element that should become focused on a page
|
||||||
|
* that does not have a <main> html tag.
|
||||||
|
* Focusing the Docusaurus Layout children is a reasonable fallback.
|
||||||
|
*/
|
||||||
|
export const SkipToContentFallbackId = 'docusaurus_skipToContent_fallback';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the skip to content element to focus when the link is clicked.
|
||||||
|
*/
|
||||||
|
function getSkipToContentTarget(): HTMLElement | null {
|
||||||
|
return (
|
||||||
|
// Try to focus the <main> in priority
|
||||||
|
// Note: this will only work if JS is enabled
|
||||||
|
// See https://github.com/facebook/docusaurus/issues/6411#issuecomment-1284136069
|
||||||
|
document.querySelector('main:first-of-type') ??
|
||||||
|
// Then try to focus the fallback element (usually the Layout children)
|
||||||
|
document.getElementById(SkipToContentFallbackId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function programmaticFocus(el: HTMLElement) {
|
||||||
|
el.setAttribute('tabindex', '-1');
|
||||||
|
el.focus();
|
||||||
|
el.removeAttribute('tabindex');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This hook wires the logic for a skip-to-content link. */
|
||||||
|
function useSkipToContent(): {
|
||||||
|
/**
|
||||||
|
* The ref to the container. On page transition, the container will be focused
|
||||||
|
* so that keyboard navigators can instantly interact with the link and jump
|
||||||
|
* to content.
|
||||||
|
*/
|
||||||
|
containerRef: React.RefObject<HTMLDivElement>;
|
||||||
|
/**
|
||||||
|
* Callback fired when the skip to content link has been clicked.
|
||||||
|
* It will programmatically focus the main content.
|
||||||
|
*/
|
||||||
|
onClick: (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||||
|
} {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const {action} = useHistory();
|
||||||
|
|
||||||
|
const onClick = useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const targetElement = getSkipToContentTarget();
|
||||||
|
if (targetElement) {
|
||||||
|
programmaticFocus(targetElement);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// "Reset" focus when navigating.
|
||||||
|
// See https://github.com/facebook/docusaurus/pull/8204#issuecomment-1276547558
|
||||||
|
useLocationChange(({location}) => {
|
||||||
|
if (containerRef.current && !location.hash && action === 'PUSH') {
|
||||||
|
programmaticFocus(containerRef.current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {containerRef, onClick};
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultSkipToContentLabel = translate({
|
||||||
|
id: 'theme.common.skipToMainContent',
|
||||||
|
description:
|
||||||
|
'The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation',
|
||||||
|
message: 'Skip to main content',
|
||||||
|
});
|
||||||
|
|
||||||
|
type SkipToContentLinkProps = Omit<ComponentProps<'a'>, 'href' | 'onClick'>;
|
||||||
|
|
||||||
|
export function SkipToContentLink(props: SkipToContentLinkProps): JSX.Element {
|
||||||
|
const linkLabel = props.children ?? DefaultSkipToContentLabel;
|
||||||
|
const {containerRef, onClick} = useSkipToContent();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
role="region"
|
||||||
|
aria-label={DefaultSkipToContentLabel}>
|
||||||
|
<a
|
||||||
|
{...props}
|
||||||
|
// Note this is a fallback href in case JS is disabled
|
||||||
|
// It has limitations, see https://github.com/facebook/docusaurus/issues/6411#issuecomment-1284136069
|
||||||
|
href={`#${SkipToContentFallbackId}`}
|
||||||
|
onClick={onClick}>
|
||||||
|
{linkLabel}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue