Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(theme-classic): fix SkipToContent without JS , refactor, make it public theming API #8204

Merged
merged 7 commits into from Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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>
);
}