Skip to content

Commit

Permalink
Add highlighting of current section to table of contents (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
delucis authored May 15, 2023
1 parent 7ee1b5d commit e96d9a7
Show file tree
Hide file tree
Showing 12 changed files with 238 additions and 80 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-pillows-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@astrojs/starlight": patch
---

Fix CSS ordering issue caused by imports in 404 route.
5 changes: 5 additions & 0 deletions .changeset/clever-parrots-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@astrojs/starlight": patch
---

Highlight current page section in table of contents.
5 changes: 5 additions & 0 deletions .changeset/slow-lies-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/starlight': patch
---

Fix usage of `aria-current` in navigation sidebar to use `page` value.
22 changes: 14 additions & 8 deletions packages/starlight/404.astro
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion packages/starlight/components/RightSidebarPanel.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions packages/starlight/components/SidebarSublist.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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>
) : (
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 11 additions & 4 deletions packages/starlight/components/TableOfContents.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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));
Expand All @@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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>
18 changes: 5 additions & 13 deletions packages/starlight/components/TableOfContents/generateToC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand Down Expand Up @@ -35,30 +36,21 @@ 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) {
throw new Error(`Orphan heading found: ${heading.text}.`);
}
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: [] });
}
}
}
Expand Down
Loading

0 comments on commit e96d9a7

Please sign in to comment.