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:
parent
12236dbc06
commit
fcfd166f2d
5 changed files with 115 additions and 43 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue