fix: Docs Site - Table of contents highlight not working (#5411)

* fix: Docs Site - Table of contents highlight not working

* Add html-escaper devDep

* add html-escaper via pnpm
This commit is contained in:
Rishi Raj Jain 2022-11-17 21:18:57 +05:30 committed by GitHub
parent 12236dbc06
commit fcfd166f2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 115 additions and 43 deletions

View file

@ -24,5 +24,8 @@
"@types/react": "^17.0.45",
"@types/node": "^18.0.0",
"@types/react-dom": "^18.0.0"
},
"devDependencies": {
"html-escaper": "^3.0.3"
}
}

View file

@ -14,7 +14,7 @@ const showMoreSection = CONFIG.COMMUNITY_INVITE_URL;
<ul>
{
editHref && (
<li class={`heading-link depth-2`}>
<li class={`header-link depth-2`}>
<a class="edit-on-github" href={editHref} target="_blank">
<svg
aria-hidden="true"
@ -40,7 +40,7 @@ const showMoreSection = CONFIG.COMMUNITY_INVITE_URL;
}
{
CONFIG.COMMUNITY_INVITE_URL && (
<li class={`heading-link depth-2`}>
<li class={`header-link depth-2`}>
<a href={CONFIG.COMMUNITY_INVITE_URL} target="_blank">
<svg
aria-hidden="true"

View file

@ -1,6 +1,7 @@
import { unescape } from 'html-escaper';
import type { MarkdownHeading } from 'astro';
import type { FunctionalComponent } from 'preact';
import { useState, useEffect, useRef } from 'preact/hooks';
import type { MarkdownHeading } from 'astro';
type ItemOffsets = {
id: string;
@ -10,9 +11,10 @@ type ItemOffsets = {
const TableOfContents: FunctionalComponent<{ headings: MarkdownHeading[] }> = ({
headings = [],
}) => {
const toc = useRef<HTMLUListElement>();
const onThisPageID = 'on-this-page-heading';
const itemOffsets = useRef<ItemOffsets[]>([]);
// FIXME: Not sure what this state is doing. It was never set to anything truthy.
const [activeId] = useState<string>('');
const [currentID, setCurrentID] = useState('overview');
useEffect(() => {
const getItemOffsets = () => {
const titles = document.querySelectorAll('article :is(h1, h2, h3, h4)');
@ -30,22 +32,57 @@ const TableOfContents: FunctionalComponent<{ headings: MarkdownHeading[] }> = ({
};
}, []);
useEffect(() => {
if (!toc.current) return;
const setCurrent: IntersectionObserverCallback = (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const { id } = entry.target;
if (id === onThisPageID) continue;
setCurrentID(entry.target.id);
break;
}
}
};
const observerOptions: IntersectionObserverInit = {
// Negative top margin accounts for `scroll-margin`.
// Negative bottom margin means heading needs to be towards top of viewport to trigger intersection.
rootMargin: '-100px 0% -66%',
threshold: 1,
};
const headingsObserver = new IntersectionObserver(setCurrent, observerOptions);
// Observe all the headings in the main page content.
document.querySelectorAll('article :is(h1,h2,h3)').forEach((h) => headingsObserver.observe(h));
// Stop observing when the component is unmounted.
return () => headingsObserver.disconnect();
}, [toc.current]);
const onLinkClick = (e) => {
setCurrentID(e.target.getAttribute('href').replace('#', ''));
};
return (
<>
<h2 className="heading">On this page</h2>
<ul>
<li className={`heading-link depth-2 ${activeId === 'overview' ? 'active' : ''}`.trim()}>
<a href="#overview">Overview</a>
</li>
<h2 id={onThisPageID} className="heading">
On this page
</h2>
<ul ref={toc}>
{headings
.filter(({ depth }) => depth > 1 && depth < 4)
.map((heading) => (
<li
className={`heading-link depth-${heading.depth} ${
activeId === heading.slug ? 'active' : ''
className={`header-link depth-${heading.depth} ${
currentID === heading.slug ? 'current-header-link' : ''
}`.trim()}
>
<a href={`#${heading.slug}`}>{heading.text}</a>
<a href={`#${heading.slug}`} onClick={onLinkClick}>
{unescape(heading.text)}
</a>
</li>
))}
</ul>

View file

@ -311,45 +311,57 @@ h2.heading {
margin-bottom: 0.5rem;
}
.heading-link {
font-size: 1rem;
padding: 0.1rem 0 0.1rem 1rem;
.header-link {
font-size: 1em;
transition: border-inline-start-color 100ms ease-out, background-color 200ms ease-out;
border-left: 4px solid var(--theme-divider);
}
.heading-link:hover,
.heading-link:focus {
border-left-color: var(--theme-accent);
color: var(--theme-accent);
}
.heading-link:focus-within {
color: var(--theme-text-light);
border-left-color: hsla(var(--color-gray-40), 1);
}
.heading-link svg {
opacity: 0.6;
}
.heading-link:hover svg {
opacity: 0.8;
}
.heading-link a {
.header-link a {
display: inline-flex;
gap: 0.5em;
width: 100%;
padding: 0.15em 0 0.15em 0;
}
.heading-link.depth-3 {
padding-left: 2rem;
}
.heading-link.depth-4 {
padding-left: 3rem;
}
.heading-link a {
font: inherit;
padding: 0.4rem 0;
line-height: 1.3;
color: inherit;
text-decoration: none;
unicode-bidi: plaintext;
}
@media (min-width: 50em) {
.header-link a {
padding: 0.275rem 0;
}
}
.header-link:hover,
.header-link:focus,
.header-link:focus-within {
border-inline-start-color: var(--theme-accent-secondary);
}
.header-link:hover a,
.header-link a:focus {
color: var(--theme-text);
text-decoration: underline;
}
.header-link svg {
opacity: 0.6;
}
.header-link:hover svg {
opacity: 0.8;
}
/* Add line and padding on the left side */
.header-link {
padding-inline-start: 1rem;
}
.header-link.depth-3 {
padding-inline-start: 2rem;
}
.header-link.depth-4 {
padding-inline-start: 3rem;
}
/* Screenreader Only Text */
@ -380,3 +392,20 @@ h2.heading {
:target {
scroll-margin: calc(var(--theme-sidebar-offset, 5rem) + 2rem) 0 2rem;
}
/* Highlight TOC header link matching the current scroll position */
.current-header-link {
background-color: var(--theme-bg-accent);
/* Indicates the current heading for forced colors users in older browsers */
outline: 1px solid transparent;
}
@media (forced-colors: active) {
.current-header-link {
border: 1px solid CanvasText;
}
}
.current-header-link a {
color: var(--theme-text);
}

View file

@ -103,6 +103,7 @@ importers:
'@types/react': ^17.0.45
'@types/react-dom': ^18.0.0
astro: ^1.6.9
html-escaper: ^3.0.3
preact: ^10.7.3
react: ^18.1.0
react-dom: ^18.1.0
@ -119,6 +120,8 @@ importers:
preact: 10.11.3
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
devDependencies:
html-escaper: 3.0.3
examples/framework-alpine:
specifiers: