summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Swithinbank2023-05-15 18:30:52 +0200
committerGitHub2023-05-15 18:30:52 +0200
commite96d9a7628c5c04fe34dbc65ddd6fabdc0667a6d (patch)
tree454cb26034b1b78603bd0de1a391431ddd970806
parent7ee1b5db9b6ec290ac7678d08b1f8d6a116d6d02 (diff)
downloadIT.starlight-e96d9a7628c5c04fe34dbc65ddd6fabdc0667a6d.tar.gz
IT.starlight-e96d9a7628c5c04fe34dbc65ddd6fabdc0667a6d.tar.bz2
IT.starlight-e96d9a7628c5c04fe34dbc65ddd6fabdc0667a6d.zip
Add highlighting of current section to table of contents (#47)
-rw-r--r--.changeset/beige-pillows-tan.md5
-rw-r--r--.changeset/clever-parrots-chew.md5
-rw-r--r--.changeset/slow-lies-attack.md5
-rw-r--r--packages/starlight/404.astro22
-rw-r--r--packages/starlight/components/RightSidebarPanel.astro4
-rw-r--r--packages/starlight/components/SidebarSublist.astro8
-rw-r--r--packages/starlight/components/TableOfContents.astro15
-rw-r--r--packages/starlight/components/TableOfContents/MobileTableOfContents.astro104
-rw-r--r--packages/starlight/components/TableOfContents/TableOfContentsList.astro39
-rw-r--r--packages/starlight/components/TableOfContents/generateToC.ts18
-rw-r--r--packages/starlight/components/TableOfContents/starlight-toc.ts85
-rw-r--r--packages/starlight/index.astro8
12 files changed, 238 insertions, 80 deletions
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;
<ThemeProvider />
<PageFrame>
<Header slot="header" locale={locale} />
- <main id="starlight__overview">
+ <main>
<MarkdownContent>
- <h1>404</h1>
+ <h1 id="starlight__overview">404</h1>
<p>Houston, we have a problem.</p>
<p>
We couldn’t find that link. Check the address or <a
@@ -40,7 +46,7 @@ const locale = undefined;
</PageFrame>
<style>
- #starlight__overview {
+ main {
margin: auto;
padding: clamp(2rem, 10vmin, 6rem) var(--sl-nav-pad-x);
max-width: var(--sl-content-width);
diff --git a/packages/starlight/components/RightSidebarPanel.astro b/packages/starlight/components/RightSidebarPanel.astro
index 81ce2e4d..c96b7a9e 100644
--- a/packages/starlight/components/RightSidebarPanel.astro
+++ b/packages/starlight/components/RightSidebarPanel.astro
@@ -16,8 +16,10 @@
}
.right-sidebar-panel :global(h2) {
color: var(--sl-color-white);
- font-size: var(--sl-text-base);
+ font-size: var(--sl-text-h5);
font-weight: 600;
+ line-height: var(--sl-line-height-headings);
+ margin-bottom: 0.5rem;
}
.right-sidebar-panel :global(a) {
display: block;
diff --git a/packages/starlight/components/SidebarSublist.astro b/packages/starlight/components/SidebarSublist.astro
index 2065c889..344df808 100644
--- a/packages/starlight/components/SidebarSublist.astro
+++ b/packages/starlight/components/SidebarSublist.astro
@@ -11,7 +11,7 @@ interface Props {
Astro.props.sublist.map((entry) => (
<li class:list={{ 'sidebar-group': entry.type === 'group' }}>
{entry.type === 'link' ? (
- <a href={entry.href} aria-current={entry.isCurrent && 'true'}>
+ <a href={entry.href} aria-current={entry.isCurrent && 'page'}>
{entry.label}
</a>
) : (
@@ -56,9 +56,9 @@ interface Props {
color: var(--sl-color-white);
}
- [aria-current='true'],
- [aria-current='true']:hover,
- [aria-current='true']:focus {
+ [aria-current='page'],
+ [aria-current='page']:hover,
+ [aria-current='page']:focus {
font-weight: 600;
color: var(--sl-color-text-invert);
background-color: var(--sl-color-text-accent);
diff --git a/packages/starlight/components/TableOfContents.astro b/packages/starlight/components/TableOfContents.astro
index 3732bce5..eff17903 100644
--- a/packages/starlight/components/TableOfContents.astro
+++ b/packages/starlight/components/TableOfContents.astro
@@ -11,7 +11,14 @@ interface Props {
const toc = generateToC(Astro.props.headings, config.tableOfContents);
---
-<nav aria-labelledby="starlight__on-this-page">
- <h2 id="starlight__on-this-page">On this page</h2>
- <TableOfContentsList toc={toc} />
-</nav>
+<starlight-toc
+ data-min-h={config.tableOfContents.minHeadingLevel}
+ data-max-h={config.tableOfContents.maxHeadingLevel}
+>
+ <nav aria-labelledby="starlight__on-this-page">
+ <h2 id="starlight__on-this-page">On this page</h2>
+ <TableOfContentsList toc={toc} />
+ </nav>
+</starlight-toc>
+
+<script src="./TableOfContents/starlight-toc"></script>
diff --git a/packages/starlight/components/TableOfContents/MobileTableOfContents.astro b/packages/starlight/components/TableOfContents/MobileTableOfContents.astro
index 494b1d00..9cb36c6e 100644
--- a/packages/starlight/components/TableOfContents/MobileTableOfContents.astro
+++ b/packages/starlight/components/TableOfContents/MobileTableOfContents.astro
@@ -12,19 +12,25 @@ interface Props {
const toc = generateToC(Astro.props.headings, config.tableOfContents);
---
-<nav aria-labelledby="starlight__on-this-page--mobile" class="lg:hidden">
- <details id="starlight__mobile-toc">
- <summary id="starlight__on-this-page--mobile" class="flex">
- <div class="toggle flex">
- On this page
- <Icon name={'right-caret'} class="caret" size="1rem" />
+<mobile-starlight-toc
+ data-min-h={config.tableOfContents.minHeadingLevel}
+ data-max-h={config.tableOfContents.maxHeadingLevel}
+>
+ <nav aria-labelledby="starlight__on-this-page--mobile" class="lg:hidden">
+ <details id="starlight__mobile-toc">
+ <summary id="starlight__on-this-page--mobile" class="flex">
+ <div class="toggle flex">
+ On this page
+ <Icon name={'right-caret'} class="caret" size="1rem" />
+ </div>
+ <span class="display-current">{toc[0]?.text}</span>
+ </summary>
+ <div class="dropdown">
+ <TableOfContentsList toc={toc} isMobile />
</div>
- </summary>
- <div class="dropdown">
- <TableOfContentsList toc={toc} isMobile />
- </div>
- </details>
-</nav>
+ </details>
+ </nav>
+</mobile-starlight-toc>
<style>
nav {
@@ -41,9 +47,12 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
}
summary {
+ gap: 0.5rem;
+ align-items: center;
height: var(--sl-mobile-toc-height);
border-bottom: 1px solid var(--sl-color-hairline-shade);
padding: 0.5rem 1rem;
+ font-size: var(--sl-text-xs);
outline-offset: var(--sl-outline-offset-inside);
}
summary::marker,
@@ -52,6 +61,7 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
}
.toggle {
+ flex-shrink: 0;
gap: 1rem;
align-items: center;
justify-content: space-between;
@@ -60,7 +70,6 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
padding-block: 0.5rem;
padding-inline-start: 0.75rem;
padding-inline-end: 0.5rem;
- font-size: var(--sl-text-xs);
line-height: 1;
background-color: var(--sl-color-black);
user-select: none;
@@ -82,6 +91,13 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
transform: rotateZ(90deg);
}
+ .display-current {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ color: var(--sl-color-white);
+ }
+
.dropdown {
--border-top: 1px;
margin-top: calc(-1 * var(--border-top));
@@ -95,31 +111,43 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
</style>
<script>
- const details = document.querySelector<HTMLDetailsElement>(
- '#starlight__mobile-toc'
- );
- if (details) {
- const closeToC = () => {
- details.open = false;
- };
- // Close the table of contents whenever a link is clicked.
- details.querySelectorAll('a').forEach((a) => {
- a.addEventListener('click', closeToC);
- });
- // Close the table of contents when a user clicks outside of it.
- window.addEventListener('click', (e) => {
- if (!details.contains(e.target as Node)) closeToC();
- });
- // Or when they press the escape key.
- window.addEventListener('keydown', (e) => {
- if (e.key === 'Escape' && details.open) {
- const hasFocus = details.contains(document.activeElement);
- closeToC();
- if (hasFocus) {
- const summary = details.querySelector('summary');
- if (summary) summary.focus();
+ import { StarlightTOC } from './starlight-toc';
+
+ class MobileStarlightTOC extends StarlightTOC {
+ override set current(link: HTMLAnchorElement) {
+ super.current = link;
+ const display = this.querySelector('.display-current') as HTMLSpanElement;
+ if (display) display.textContent = link.textContent;
+ }
+
+ constructor() {
+ super();
+ const details = this.querySelector('details');
+ if (!details) return;
+ const closeToC = () => {
+ details.open = false;
+ };
+ // Close the table of contents whenever a link is clicked.
+ details.querySelectorAll('a').forEach((a) => {
+ a.addEventListener('click', closeToC);
+ });
+ // Close the table of contents when a user clicks outside of it.
+ window.addEventListener('click', (e) => {
+ if (!details.contains(e.target as Node)) closeToC();
+ });
+ // Or when they press the escape key.
+ window.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && details.open) {
+ const hasFocus = details.contains(document.activeElement);
+ closeToC();
+ if (hasFocus) {
+ const summary = details.querySelector('summary');
+ if (summary) summary.focus();
+ }
}
- }
- });
+ });
+ }
}
+
+ customElements.define('mobile-starlight-toc', MobileStarlightTOC);
</script>
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) => (
<li>
- <a href={'#' + heading.slug}>{heading.text}</a>
+ <a href={'#' + heading.slug} aria-current={heading.current && 'true'}>
+ <span>{heading.text}</span>
+ </a>
{heading.children.length > 0 && (
<Astro.self
toc={heading.children}
@@ -30,27 +32,52 @@ const { toc, isMobile = false, depth = 0 } = Astro.props;
<style define:vars={{ depth }}>
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==');
+ }
</style>
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);
<MobileTableOfContents headings={headings} />
<TwoColumnContent>
<RightSidebar slot="right-sidebar" entry={entry} headings={headings} />
- <main
- id="starlight__overview"
- data-pagefind-body
- lang={entryMeta.lang}
- dir={entryMeta.dir}
- >
+ <main data-pagefind-body lang={entryMeta.lang} dir={entryMeta.dir}>
<ContentPanel>
<h1
+ id="starlight__overview"
style="font-size: var(--sl-text-h1); line-height: var(--sl-line-height-headings); font-weight: 600; color: var(--sl-color-white); margin-top: 1rem;"
>
{entry.data.title}