From db4f170629ff67bbfead8ac8093985612c773965 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Tue, 13 Oct 2026 10:25:23 +0000 Subject: [PATCH 1/3] Fix Aliki TOC highlighting and method group nesting The TOC highlight only changed when a heading crossed the viewport, so it went stale after long jumps and stayed empty when landing on a method link. Compute the current heading on scroll instead, falling back to the last heading above the viewport. Constants, Attributes and method group headings in the untitled section are now listed at the same TOC level as the class comment's sections, instead of nesting under its last heading. --- lib/rdoc/generator/template/aliki/js/aliki.js | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/lib/rdoc/generator/template/aliki/js/aliki.js b/lib/rdoc/generator/template/aliki/js/aliki.js index 2855cabb75..6776b9fa5f 100644 --- a/lib/rdoc/generator/template/aliki/js/aliki.js +++ b/lib/rdoc/generator/template/aliki/js/aliki.js @@ -267,8 +267,7 @@ function generateToc() { if (!heading.id) return; const li = document.createElement('li'); - const level = heading.tagName.toLowerCase(); - li.className = `toc-item toc-${level}`; + li.className = `toc-item toc-${tocLevel(heading)}`; const link = document.createElement('a'); link.href = `#${heading.id}`; @@ -292,6 +291,17 @@ function generateToc() { } } +// Constants, Attributes and method group headings are h3 so they nest under a +// titled section's h2. In the untitled section, list them as h2 so they don't +// look like children of the class comment's last heading. +function tocLevel(heading) { + const level = heading.tagName.toLowerCase(); + if (level !== 'h3' || !heading.matches('.documentation-section > section > header > h3')) return level; + + const section = heading.closest('.documentation-section'); + return section.querySelector(':scope > .documentation-section-title') ? level : 'h2'; +} + function hookTocActiveHighlighting() { const tocLinks = document.querySelectorAll('.toc-link'); const targetHeadings = []; @@ -305,28 +315,25 @@ function hookTocActiveHighlighting() { if (targetHeadings.length === 0) return; - const observerOptions = { - root: null, - rootMargin: '0% 0px -35% 0px', - threshold: 0 - }; - - const intersectingHeadings = new Set(); + let activeLink = null; const update = () => { - const firstIntersectingHeading = targetHeadings.find((heading) => { - return intersectingHeadings.has(heading); + // Prefer the first heading in the upper 65% of the viewport. With no + // heading there, such as deep inside a long method list, the current + // section is the last heading above the viewport. + const viewportLimit = window.innerHeight * 0.65; + const currentHeading = targetHeadings.find((heading) => { + const rect = heading.getBoundingClientRect(); + return rect.bottom > 0 && rect.top < viewportLimit; + }) || targetHeadings.findLast((heading) => { + return heading.getBoundingClientRect().top < 0; }); - if (!firstIntersectingHeading) return; - const correspondingLink = document.querySelector(`.toc-link[data-target="${firstIntersectingHeading.id}"]`); - if (!correspondingLink) return; + if (!currentHeading) return; + const correspondingLink = document.querySelector(`.toc-link[data-target="${currentHeading.id}"]`); + if (!correspondingLink || correspondingLink === activeLink) return; - // Remove active class from all links - tocLinks.forEach((link) => { - link.classList.remove('active'); - }); - - // Add active class to current link + activeLink?.classList.remove('active'); correspondingLink.classList.add('active'); + activeLink = correspondingLink; // Scroll link into view if needed const tocNav = document.querySelector('#toc-nav'); @@ -339,21 +346,18 @@ function hookTocActiveHighlighting() { } } }; - const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - intersectingHeadings.add(entry.target); - } else { - intersectingHeadings.delete(entry.target); - } + + let updateScheduled = false; + window.addEventListener('scroll', () => { + if (updateScheduled) return; + updateScheduled = true; + requestAnimationFrame(() => { + updateScheduled = false; + update(); }); - update(); - }, observerOptions); + }, { passive: true }); - // Observe all headings that have corresponding TOC links - targetHeadings.forEach((heading) => { - observer.observe(heading); - }); + update(); } function setHeadingScrollHandler(heading, link) { From 8f39e09b9cad79b355dae459b2c052dbaa2d124d Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Tue, 13 Oct 2026 11:21:08 +0000 Subject: [PATCH 2/3] Leave Aliki TOC nesting to the heading level change ruby/rdoc#1842 fixes the method group nesting by emitting h2 headings in the default section, so the TOC needs no special case for them. --- lib/rdoc/generator/template/aliki/js/aliki.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/lib/rdoc/generator/template/aliki/js/aliki.js b/lib/rdoc/generator/template/aliki/js/aliki.js index 6776b9fa5f..20886c7cca 100644 --- a/lib/rdoc/generator/template/aliki/js/aliki.js +++ b/lib/rdoc/generator/template/aliki/js/aliki.js @@ -267,7 +267,8 @@ function generateToc() { if (!heading.id) return; const li = document.createElement('li'); - li.className = `toc-item toc-${tocLevel(heading)}`; + const level = heading.tagName.toLowerCase(); + li.className = `toc-item toc-${level}`; const link = document.createElement('a'); link.href = `#${heading.id}`; @@ -291,17 +292,6 @@ function generateToc() { } } -// Constants, Attributes and method group headings are h3 so they nest under a -// titled section's h2. In the untitled section, list them as h2 so they don't -// look like children of the class comment's last heading. -function tocLevel(heading) { - const level = heading.tagName.toLowerCase(); - if (level !== 'h3' || !heading.matches('.documentation-section > section > header > h3')) return level; - - const section = heading.closest('.documentation-section'); - return section.querySelector(':scope > .documentation-section-title') ? level : 'h2'; -} - function hookTocActiveHighlighting() { const tocLinks = document.querySelectorAll('.toc-link'); const targetHeadings = []; From 25c96638671b059e95a0eb8574da4760d58ac44e Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 15 Oct 2026 21:16:12 +0000 Subject: [PATCH 3/3] Update the Aliki TOC highlight on resize A taller viewport can bring the next heading into the upper part of the page without a scroll event. --- lib/rdoc/generator/template/aliki/js/aliki.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/rdoc/generator/template/aliki/js/aliki.js b/lib/rdoc/generator/template/aliki/js/aliki.js index 20886c7cca..6d1605a10e 100644 --- a/lib/rdoc/generator/template/aliki/js/aliki.js +++ b/lib/rdoc/generator/template/aliki/js/aliki.js @@ -338,14 +338,16 @@ function hookTocActiveHighlighting() { }; let updateScheduled = false; - window.addEventListener('scroll', () => { + const scheduleUpdate = () => { if (updateScheduled) return; updateScheduled = true; requestAnimationFrame(() => { updateScheduled = false; update(); }); - }, { passive: true }); + }; + window.addEventListener('scroll', scheduleUpdate, { passive: true }); + window.addEventListener('resize', scheduleUpdate); update(); }