From aa4fa66794907e8c563c44e19f70a7ba54909e50 Mon Sep 17 00:00:00 2001 From: mturoci <64769322+mturoci@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:27:08 +0200 Subject: [PATCH] fix(theme-classic): fix SkipToContent without JS , refactor, make it public theming API (#8204) Co-authored-by: sebastienlorber --- .../src/getSwizzleConfig.ts | 8 ++ .../src/theme/Layout/index.tsx | 7 +- .../src/theme/SkipToContent/index.tsx | 20 +--- .../src/hooks/useSkipToContent.ts | 58 ---------- packages/docusaurus-theme-common/src/index.ts | 5 + .../docusaurus-theme-common/src/internal.ts | 1 - .../src/utils/skipToContentUtils.tsx | 103 ++++++++++++++++++ 7 files changed, 124 insertions(+), 78 deletions(-) delete mode 100644 packages/docusaurus-theme-common/src/hooks/useSkipToContent.ts create mode 100644 packages/docusaurus-theme-common/src/utils/skipToContentUtils.tsx diff --git a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts index c0378ab47e42..c5f83bb3c114 100644 --- a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts +++ b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts @@ -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', diff --git a/packages/docusaurus-theme-classic/src/theme/Layout/index.tsx b/packages/docusaurus-theme-classic/src/theme/Layout/index.tsx index 26308f7eb26c..cf460364766f 100644 --- a/packages/docusaurus-theme-classic/src/theme/Layout/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Layout/index.tsx @@ -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'; @@ -42,6 +46,7 @@ export default function Layout(props: Props): JSX.Element {
- {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - - - Skip to main content - - -
- ); + return ; } diff --git a/packages/docusaurus-theme-common/src/hooks/useSkipToContent.ts b/packages/docusaurus-theme-common/src/hooks/useSkipToContent.ts deleted file mode 100644 index bdcac465d487..000000000000 --- a/packages/docusaurus-theme-common/src/hooks/useSkipToContent.ts +++ /dev/null @@ -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` 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; - /** - * Callback fired when the skip to content link has been interacted with. It - * will programmatically focus the main content. - */ - handleSkip: (e: React.MouseEvent) => void; -} { - const containerRef = useRef(null); - const {action} = useHistory(); - const handleSkip = useCallback((e: React.MouseEvent) => { - 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}; -} diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index d282c1d85316..03503ddfc155 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -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'; diff --git a/packages/docusaurus-theme-common/src/internal.ts b/packages/docusaurus-theme-common/src/internal.ts index 8415cc967bfe..aefaa2ddbabf 100644 --- a/packages/docusaurus-theme-common/src/internal.ts +++ b/packages/docusaurus-theme-common/src/internal.ts @@ -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'; diff --git a/packages/docusaurus-theme-common/src/utils/skipToContentUtils.tsx b/packages/docusaurus-theme-common/src/utils/skipToContentUtils.tsx new file mode 100644 index 000000000000..fbdf862a8c89 --- /dev/null +++ b/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
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
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; + /** + * Callback fired when the skip to content link has been clicked. + * It will programmatically focus the main content. + */ + onClick: (e: React.MouseEvent) => void; +} { + const containerRef = useRef(null); + const {action} = useHistory(); + + const onClick = useCallback((e: React.MouseEvent) => { + 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, 'href' | 'onClick'>; + +export function SkipToContentLink(props: SkipToContentLinkProps): JSX.Element { + const linkLabel = props.children ?? DefaultSkipToContentLabel; + const {containerRef, onClick} = useSkipToContent(); + return ( + + ); +}