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/react": "^17.0.45",
|
||||||
"@types/node": "^18.0.0",
|
"@types/node": "^18.0.0",
|
||||||
"@types/react-dom": "^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>
|
<ul>
|
||||||
{
|
{
|
||||||
editHref && (
|
editHref && (
|
||||||
<li class={`heading-link depth-2`}>
|
<li class={`header-link depth-2`}>
|
||||||
<a class="edit-on-github" href={editHref} target="_blank">
|
<a class="edit-on-github" href={editHref} target="_blank">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
@ -40,7 +40,7 @@ const showMoreSection = CONFIG.COMMUNITY_INVITE_URL;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
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">
|
<a href={CONFIG.COMMUNITY_INVITE_URL} target="_blank">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { unescape } from 'html-escaper';
|
||||||
|
import type { MarkdownHeading } from 'astro';
|
||||||
import type { FunctionalComponent } from 'preact';
|
import type { FunctionalComponent } from 'preact';
|
||||||
import { useState, useEffect, useRef } from 'preact/hooks';
|
import { useState, useEffect, useRef } from 'preact/hooks';
|
||||||
import type { MarkdownHeading } from 'astro';
|
|
||||||
|
|
||||||
type ItemOffsets = {
|
type ItemOffsets = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -10,9 +11,10 @@ type ItemOffsets = {
|
||||||
const TableOfContents: FunctionalComponent<{ headings: MarkdownHeading[] }> = ({
|
const TableOfContents: FunctionalComponent<{ headings: MarkdownHeading[] }> = ({
|
||||||
headings = [],
|
headings = [],
|
||||||
}) => {
|
}) => {
|
||||||
|
const toc = useRef<HTMLUListElement>();
|
||||||
|
const onThisPageID = 'on-this-page-heading';
|
||||||
const itemOffsets = useRef<ItemOffsets[]>([]);
|
const itemOffsets = useRef<ItemOffsets[]>([]);
|
||||||
// FIXME: Not sure what this state is doing. It was never set to anything truthy.
|
const [currentID, setCurrentID] = useState('overview');
|
||||||
const [activeId] = useState<string>('');
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getItemOffsets = () => {
|
const getItemOffsets = () => {
|
||||||
const titles = document.querySelectorAll('article :is(h1, h2, h3, h4)');
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2 className="heading">On this page</h2>
|
<h2 id={onThisPageID} className="heading">
|
||||||
<ul>
|
On this page
|
||||||
<li className={`heading-link depth-2 ${activeId === 'overview' ? 'active' : ''}`.trim()}>
|
</h2>
|
||||||
<a href="#overview">Overview</a>
|
<ul ref={toc}>
|
||||||
</li>
|
|
||||||
{headings
|
{headings
|
||||||
.filter(({ depth }) => depth > 1 && depth < 4)
|
.filter(({ depth }) => depth > 1 && depth < 4)
|
||||||
.map((heading) => (
|
.map((heading) => (
|
||||||
<li
|
<li
|
||||||
className={`heading-link depth-${heading.depth} ${
|
className={`header-link depth-${heading.depth} ${
|
||||||
activeId === heading.slug ? 'active' : ''
|
currentID === heading.slug ? 'current-header-link' : ''
|
||||||
}`.trim()}
|
}`.trim()}
|
||||||
>
|
>
|
||||||
<a href={`#${heading.slug}`}>{heading.text}</a>
|
<a href={`#${heading.slug}`} onClick={onLinkClick}>
|
||||||
|
{unescape(heading.text)}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -311,45 +311,57 @@ h2.heading {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading-link {
|
.header-link {
|
||||||
font-size: 1rem;
|
font-size: 1em;
|
||||||
padding: 0.1rem 0 0.1rem 1rem;
|
transition: border-inline-start-color 100ms ease-out, background-color 200ms ease-out;
|
||||||
border-left: 4px solid var(--theme-divider);
|
border-left: 4px solid var(--theme-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading-link:hover,
|
.header-link a {
|
||||||
.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 {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
width: 100%;
|
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;
|
font: inherit;
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
line-height: 1.3;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
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 */
|
/* Screenreader Only Text */
|
||||||
|
@ -380,3 +392,20 @@ h2.heading {
|
||||||
:target {
|
:target {
|
||||||
scroll-margin: calc(var(--theme-sidebar-offset, 5rem) + 2rem) 0 2rem;
|
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);
|
||||||
|
}
|
||||||
|
|
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
@ -103,6 +103,7 @@ importers:
|
||||||
'@types/react': ^17.0.45
|
'@types/react': ^17.0.45
|
||||||
'@types/react-dom': ^18.0.0
|
'@types/react-dom': ^18.0.0
|
||||||
astro: ^1.6.9
|
astro: ^1.6.9
|
||||||
|
html-escaper: ^3.0.3
|
||||||
preact: ^10.7.3
|
preact: ^10.7.3
|
||||||
react: ^18.1.0
|
react: ^18.1.0
|
||||||
react-dom: ^18.1.0
|
react-dom: ^18.1.0
|
||||||
|
@ -119,6 +120,8 @@ importers:
|
||||||
preact: 10.11.3
|
preact: 10.11.3
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0_react@18.2.0
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
devDependencies:
|
||||||
|
html-escaper: 3.0.3
|
||||||
|
|
||||||
examples/framework-alpine:
|
examples/framework-alpine:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
|
Loading…
Add table
Reference in a new issue