Skip to content

Commit

Permalink
fix(theme-classic): fix SkipToContent without JS , refactor, make it …
Browse files Browse the repository at this point in the history
…public theming API (#8204)

Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
  • Loading branch information
mturoci and slorber committed Oct 19, 2022
1 parent 22c90cb commit aa4fa66
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 78 deletions.
8 changes: 8 additions & 0 deletions packages/docusaurus-theme-classic/src/getSwizzleConfig.ts
Expand Up @@ -377,6 +377,14 @@ export default function getSwizzleConfig(): SwizzleConfig {
description:
'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': {
actions: {
eject: 'safe',
Expand Down
7 changes: 6 additions & 1 deletion packages/docusaurus-theme-classic/src/theme/Layout/index.tsx
Expand Up @@ -8,7 +8,11 @@
import React from 'react';
import clsx from 'clsx';
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 SkipToContent from '@theme/SkipToContent';
import AnnouncementBar from '@theme/AnnouncementBar';
Expand Down Expand Up @@ -42,6 +46,7 @@ export default function Layout(props: Props): JSX.Element {
<Navbar />

<div
id={SkipToContentFallbackId}
className={clsx(
ThemeClassNames.wrapper.main,
styles.mainWrapper,
Expand Down
Expand Up @@ -6,26 +6,10 @@
*/

import React from 'react';
import Translate, {translate} from '@docusaurus/Translate';
import {useSkipToContent} from '@docusaurus/theme-common/internal';
import {SkipToContentLink} from '@docusaurus/theme-common';

import styles from './styles.module.css';

export default function SkipToContent(): JSX.Element {
const {containerRef, handleSkip} = useSkipToContent();
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>
);
return <SkipToContentLink className={styles.skipToContent} />;
}
58 changes: 0 additions & 58 deletions packages/docusaurus-theme-common/src/hooks/useSkipToContent.ts

This file was deleted.

5 changes: 5 additions & 0 deletions packages/docusaurus-theme-common/src/index.ts
Expand Up @@ -81,4 +81,9 @@ export {useDocsPreferredVersion} from './contexts/docsPreferredVersion';

export {processAdmonitionProps} from './utils/admonitionUtils';

export {
SkipToContentFallbackId,
SkipToContentLink,
} from './utils/skipToContentUtils';

export {ErrorBoundaryTryAgainButton} from './utils/errorBoundaryUtils';
1 change: 0 additions & 1 deletion packages/docusaurus-theme-common/src/internal.ts
Expand Up @@ -117,6 +117,5 @@ export {
export {useLockBodyScroll} from './hooks/useLockBodyScroll';
export {useSearchPage} from './hooks/useSearchPage';
export {useCodeWordWrap} from './hooks/useCodeWordWrap';
export {useSkipToContent} from './hooks/useSkipToContent';
export {getPrismCssVariables} from './utils/codeBlockUtils';
export {useBackToTopButton} from './hooks/useBackToTopButton';
103 changes: 103 additions & 0 deletions packages/docusaurus-theme-common/src/utils/skipToContentUtils.tsx
@@ -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>
);
}

0 comments on commit aa4fa66

Please sign in to comment.