From e96d9a7628c5c04fe34dbc65ddd6fabdc0667a6d Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Mon, 15 May 2023 18:30:52 +0200 Subject: Add highlighting of current section to table of contents (#47) --- .changeset/beige-pillows-tan.md | 5 + .changeset/clever-parrots-chew.md | 5 + .changeset/slow-lies-attack.md | 5 + packages/starlight/404.astro | 22 +++-- .../starlight/components/RightSidebarPanel.astro | 4 +- packages/starlight/components/SidebarSublist.astro | 8 +- .../starlight/components/TableOfContents.astro | 15 ++- .../TableOfContents/MobileTableOfContents.astro | 104 +++++++++++++-------- .../TableOfContents/TableOfContentsList.astro | 39 ++++++-- .../components/TableOfContents/generateToC.ts | 18 +--- .../components/TableOfContents/starlight-toc.ts | 85 +++++++++++++++++ packages/starlight/index.astro | 8 +- 12 files changed, 238 insertions(+), 80 deletions(-) create mode 100644 .changeset/beige-pillows-tan.md create mode 100644 .changeset/clever-parrots-chew.md create mode 100644 .changeset/slow-lies-attack.md create mode 100644 packages/starlight/components/TableOfContents/starlight-toc.ts diff --git a/.changeset/beige-pillows-tan.md b/.changeset/beige-pillows-tan.md new file mode 100644 index 00000000..519296d2 --- /dev/null +++ b/.changeset/beige-pillows-tan.md @@ -0,0 +1,5 @@ +--- +"@astrojs/starlight": patch +--- + +Fix CSS ordering issue caused by imports in 404 route. diff --git a/.changeset/clever-parrots-chew.md b/.changeset/clever-parrots-chew.md new file mode 100644 index 00000000..89149956 --- /dev/null +++ b/.changeset/clever-parrots-chew.md @@ -0,0 +1,5 @@ +--- +"@astrojs/starlight": patch +--- + +Highlight current page section in table of contents. diff --git a/.changeset/slow-lies-attack.md b/.changeset/slow-lies-attack.md new file mode 100644 index 00000000..ce2a62e3 --- /dev/null +++ b/.changeset/slow-lies-attack.md @@ -0,0 +1,5 @@ +--- +'@astrojs/starlight': patch +--- + +Fix usage of `aria-current` in navigation sidebar to use `page` value. diff --git a/packages/starlight/404.astro b/packages/starlight/404.astro index a0f8bb4c..eee55f8b 100644 --- a/packages/starlight/404.astro +++ b/packages/starlight/404.astro @@ -1,15 +1,21 @@ --- -import Header from './components/Header.astro'; -import MarkdownContent from './components/MarkdownContent.astro'; -import ThemeProvider from './components/ThemeProvider.astro'; -import PageFrame from './layout/PageFrame.astro'; - // Built-in CSS styles. import './style/props.css'; import './style/reset.css'; import './style/shiki.css'; import './style/util.css'; +// Layout +import PageFrame from './layout/PageFrame.astro'; + +// Components +import Header from './components/Header.astro'; +import MarkdownContent from './components/MarkdownContent.astro'; +import ThemeProvider from './components/ThemeProvider.astro'; + +// Important that this is the last import so it can override built-in styles. +import 'virtual:starlight/user-css'; + // TODO: replace with proper values — requires support for a “default” locale const lang = 'en'; const dir = 'ltr'; @@ -26,9 +32,9 @@ const locale = undefined;
-
+
-

404

+

404

Houston, we have a problem.

We couldn’t find that link. Check the address or diff --git a/packages/starlight/components/TableOfContents/TableOfContentsList.astro b/packages/starlight/components/TableOfContents/TableOfContentsList.astro index f19c91bd..83adf903 100644 --- a/packages/starlight/components/TableOfContents/TableOfContentsList.astro +++ b/packages/starlight/components/TableOfContents/TableOfContentsList.astro @@ -14,7 +14,9 @@ const { toc, isMobile = false, depth = 0 } = Astro.props; { toc.map((heading) => (

  • - {heading.text} + + {heading.text} + {heading.children.length > 0 && ( ul { padding: 0; - } - ul :global(::marker) { - color: transparent; + list-style: none; } a { - --pad-inline: 0rem; + --pad-inline: 0.5rem; display: block; + border-radius: 0.25rem; + padding-block: 0.25rem; padding-inline: calc(1rem * var(--depth) + var(--pad-inline)) var(--pad-inline); + line-height: 1.25; + } + a[aria-current='true'], + a[aria-current='true']:hover, + a[aria-current='true']:focus { + font-weight: 600; + color: var(--sl-color-text-invert); + background-color: var(--sl-color-text-accent); } .isMobile a { --pad-inline: 1rem; + display: flex; + justify-content: space-between; + gap: var(--pad-inline); border-top: 1px solid var(--sl-color-gray-6); + border-radius: 0; padding-block: 0.5rem; color: var(--sl-color-text); font-size: var(--sl-text-sm); - line-height: 1.25; text-decoration: none; outline-offset: var(--sl-outline-offset-inside); } .isMobile:first-child > li:first-child > a { border-top: 0; } + .isMobile a[aria-current='true'], + .isMobile a[aria-current='true']:hover, + .isMobile a[aria-current='true']:focus { + color: var(--sl-color-white); + background-color: unset; + } + .isMobile a[aria-current='true']::after { + content: ''; + width: 1rem; + background-color: var(--sl-color-text-accent); + /* Check mark SVG icon */ + -webkit-mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNCAxNCc+PHBhdGggZD0nTTEwLjkxNCA0LjIwNmEuNTgzLjU4MyAwIDAgMC0uODI4IDBMNS43NCA4LjU1NyAzLjkxNCA2LjcyNmEuNTk2LjU5NiAwIDAgMC0uODI4Ljg1N2wyLjI0IDIuMjRhLjU4My41ODMgMCAwIDAgLjgyOCAwbDQuNzYtNC43NmEuNTgzLjU4MyAwIDAgMCAwLS44NTdaJy8+PC9zdmc+Cg=='); + mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNCAxNCc+PHBhdGggZD0nTTEwLjkxNCA0LjIwNmEuNTgzLjU4MyAwIDAgMC0uODI4IDBMNS43NCA4LjU1NyAzLjkxNCA2LjcyNmEuNTk2LjU5NiAwIDAgMC0uODI4Ljg1N2wyLjI0IDIuMjRhLjU4My41ODMgMCAwIDAgLjgyOCAwbDQuNzYtNC43NmEuNTgzLjU4MyAwIDAgMCAwLS44NTdaJy8+PC9zdmc+Cg=='); + } diff --git a/packages/starlight/components/TableOfContents/generateToC.ts b/packages/starlight/components/TableOfContents/generateToC.ts index 155eb285..a6c54765 100644 --- a/packages/starlight/components/TableOfContents/generateToC.ts +++ b/packages/starlight/components/TableOfContents/generateToC.ts @@ -2,6 +2,7 @@ import type { MarkdownHeading } from 'astro'; export interface TocItem extends MarkdownHeading { children: TocItem[]; + current?: boolean; } function diveChildren(item: TocItem, depth: number): TocItem[] { @@ -35,10 +36,7 @@ export function generateToC( for (const heading of headings) { if (toc.length === 0) { - toc.push({ - ...heading, - children: [], - }); + toc.push({ ...heading, children: [], current: true }); } else { const lastItemInToc = toc.at(-1)!; if (heading.depth < lastItemInToc.depth) { @@ -46,19 +44,13 @@ export function generateToC( } if (heading.depth === lastItemInToc.depth) { // same depth - toc.push({ - ...heading, - children: [], - }); + toc.push({ ...heading, children: [] }); } else { // higher depth - // push into children, or children' children alike + // push into children, or children's children alike const gap = heading.depth - lastItemInToc.depth; const target = diveChildren(lastItemInToc, gap); - target.push({ - ...heading, - children: [], - }); + target.push({ ...heading, children: [] }); } } } diff --git a/packages/starlight/components/TableOfContents/starlight-toc.ts b/packages/starlight/components/TableOfContents/starlight-toc.ts new file mode 100644 index 00000000..450fad08 --- /dev/null +++ b/packages/starlight/components/TableOfContents/starlight-toc.ts @@ -0,0 +1,85 @@ +export class StarlightTOC extends HTMLElement { + private _current = this.querySelector( + 'a[aria-current="true"]' + ) as HTMLAnchorElement | null; + private minH = parseInt(this.dataset.minH || '2', 10); + private maxH = parseInt(this.dataset.maxH || '3', 10); + + protected set current(link: HTMLAnchorElement) { + if (link === this._current) return; + if (this._current) this._current.removeAttribute('aria-current'); + link.setAttribute('aria-current', 'true'); + this._current = link; + } + + constructor() { + super(); + + /** All the links in the table of contents. */ + const links = [...this.querySelectorAll('a')]; + + /** Test if an element is a table-of-contents heading. */ + const isHeading = (el: Element): el is HTMLHeadingElement => { + if (el instanceof HTMLHeadingElement) { + // Special case for page title h1 + if (el.id === 'starlight__overview') return true; + // Check the heading level is within the user-configured limits for the ToC + const level = el.tagName[1]; + if (level) { + const int = parseInt(level, 10); + if (int >= this.minH && int <= this.maxH) return true; + } + } + return false; + }; + + /** Walk up the DOM to find the nearest heading. */ + const getElementHeading = ( + el: Element | null + ): HTMLHeadingElement | null => { + if (!el) return null; + const origin = el; + while (el) { + if (isHeading(el)) return el; + // Assign the previous sibling’s last, most deeply nested child to el. + el = el.previousElementSibling; + while (el?.lastElementChild) { + el = el.lastElementChild; + } + // Look for headings amongst siblings. + const h = getElementHeading(el); + if (h) return h; + } + // Walk back up the parent. + return getElementHeading(origin.parentElement); + }; + + /** Handle intersections and set the current link to the heading for the current intersection. */ + const setCurrent: IntersectionObserverCallback = (entries) => { + for (const { isIntersecting, target } of entries) { + if (!isIntersecting) continue; + const heading = getElementHeading(target); + if (!heading) continue; + const link = links.find((link) => link.hash === '#' + heading.id); + if (link) { + this.current = link; + break; + } + } + }; + + const headingsObserver = new IntersectionObserver(setCurrent, { + rootMargin: '5% 0% -85%', + }); + + // Observe elements with an `id` (most likely headings) and their siblings. + // Also observe direct children of `.content` to include elements before + // the first heading. + const toObserve = document.querySelectorAll( + 'main [id], main [id] ~ *, main .content > *' + ); + toObserve.forEach((h) => headingsObserver.observe(h)); + } +} + +customElements.define('starlight-toc', StarlightTOC); diff --git a/packages/starlight/index.astro b/packages/starlight/index.astro index 102677e7..cf2ce4db 100644 --- a/packages/starlight/index.astro +++ b/packages/starlight/index.astro @@ -57,14 +57,10 @@ const prevNextLinks = getPrevNextLinks(sidebar); -
    +

    {entry.data.title} -- cgit