-
-
Notifications
You must be signed in to change notification settings - Fork 9.9k
Expand file tree
/
Copy pathskipToContentUtils.tsx
More file actions
108 lines (97 loc) · 3.53 KB
/
skipToContentUtils.tsx
File metadata and controls
108 lines (97 loc) · 3.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/**
* 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,
type ReactNode,
} 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.
*
* __ prefix allows search crawlers (Algolia/DocSearch) to ignore anchors
* https://github.com/facebook/docusaurus/issues/8883#issuecomment-1516328368
*/
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 skip link anchor. On page transition, the anchor will be
* focused so that keyboard navigators can instantly interact with the link
* and jump to content.
*/
anchorRef: React.RefObject<HTMLAnchorElement | null>;
/**
* 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 anchorRef = useRef<HTMLAnchorElement>(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 (anchorRef.current && !location.hash && action === 'PUSH') {
programmaticFocus(anchorRef.current);
}
});
return {anchorRef, 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): ReactNode {
const linkLabel = props.children ?? DefaultSkipToContentLabel;
const {anchorRef, onClick} = useSkipToContent();
// eslint-disable-next-line @docusaurus/no-html-links
return (
<a
{...props}
ref={anchorRef}
// 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>
);
}