Skip to content

Conversation

sharadregoti
Copy link
Contributor

@sharadregoti sharadregoti commented Sep 26, 2025

PR Type

Enhancement, Documentation


Description

  • Add collapsible TOC with toggle buttons

  • Normalize TOC depth and indentation

  • SPA-safe re-init via MutationObserver

  • Minor release notes formatting tweaks


Diagram Walkthrough

flowchart LR
  A["toc.js: init()"] -- "on load + SPA changes" --> B["run()"]
  B["run()"] -- "normalize depths, indent" --> C["setup items"]
  C["setup items"] -- "add toggle buttons" --> D["collapse/expand logic"]
  D["collapse/expand logic"] -- "show/hide descendants" --> E["updated TOC UI"]
  A["toc.js: init()"] -- "MutationObserver" --> B["run()"]
  F["toc.css"] -- "styles for row/toggle" --> E["updated TOC UI"]
Loading

File Walkthrough

Relevant files
Enhancement
toc.css
Styles for collapsible TOC and toggle UI                                 

toc.css

  • Style TOC container and items
  • Add flex row for anchor/toggle
  • Define toggle button focus/rotation styles
  • Truncate long anchors with ellipsis
+57/-0   
toc.js
Collapsible TOC logic with SPA observer                                   

toc.js

  • Initialize TOC enhancement once per page
  • Normalize item depths; indent anchors
  • Insert toggle buttons; collapse/expand descendants
  • Observe SPA mutations to re-run safely
+131/-0 
Documentation
dashboard.mdx
Minor formatting in dashboard release notes                           

developer-support/release-notes/dashboard.mdx

  • Add blank lines for layout consistency
  • No content changes to notes
+4/-0     
helm-chart.mdx
Spacing adjustments in helm chart notes                                   

developer-support/release-notes/helm-chart.mdx

  • Insert spacing before section headings
  • No semantic content changes
+2/-0     

Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

The MutationObserver reconnects to document.body after run(), but run() sets toc.dataset.enhanced='true' and the observer callback checks for a falsy enhanced flag. If SPA navigation replaces the inner HTML of the existing TOC element (keeping the element itself), enhanced remains 'true' and run() will never re-execute, potentially leaving a stale TOC. Consider clearing the flag when the TOC subtree is replaced or detecting content changes rather than presence.

    // Reconnect the observer after DOM modifications are complete
    if (observer) {
        observer.observe(document.body, { childList: true, subtree: true });
    }
}

function init() {
    run(); // Run on initial load

    // Set up an observer to re-run the script on SPA navigation
    observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            if (mutation.addedNodes.length > 0) {
                const toc = document.getElementById('table-of-contents-content');
                // If a TOC exists and it hasn't been enhanced yet, run the script
                if (toc && !toc.dataset.enhanced) {
                    run();
                    break; // Found and processed a new TOC, no need to check other mutations
                }
            }
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
}
Accessibility

Toggle buttons are injected without an accessible name; screen readers will announce them generically. Add an aria-label describing the section (e.g., "Collapse/expand ") or use aria-controls with an ID on the controlled subtree. Also update aria-expanded on initial render to reflect the current visual state.

const toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'toc-toggle';
toggle.setAttribute('aria-expanded', 'true');
toggle.innerHTML = `
    <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true" focusable="false">
      <path d="M8.5 10.5 L12 14 L15.5 10.5" stroke="currentColor" stroke-width="1.6" fill="none"
            stroke-linecap="round" stroke-linejoin="round"/>
    </svg>
  `;

const anchor = li.querySelector('a');
if (anchor) {
    const wrapper = document.createElement('div');
UX Consistency

The rotated state for the chevron is applied when expanded via class 'rotated', but initial buttons are created with aria-expanded="true" without adding the rotated class. This causes a mismatch on first render until clicked. Ensure initial class matches initial expanded state.

/* Rotated state for chevron when collapsed */
.toc-toggle.rotated svg {
    transform: rotate(-90deg);

Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Use live DOM traversal for subtree

Because items is captured once, DOM changes (inserting wrappers/toggles) can desync
indices and depth traversal, causing incorrect hiding/showing. Recompute the
descendant range at click time by walking siblings from the live DOM until a
shallower-or-equal depth is reached. This avoids stale indices and ensures correct
subtree visibility.

toc.js [33-99]

-const items = Array.from(toc.querySelectorAll('.toc-item'));
-...
+const getDepth = el => Number(el.dataset.depth);
 // Add toggle buttons and functionality
-items.forEach((li, idx) => {
-  const depth = Number(li.dataset.depth);
-  ...
+items.forEach((li) => {
+  const depth = getDepth(li);
+  const nextLi = (node) => {
+    let n = node.nextElementSibling;
+    while (n && !n.classList.contains('toc-item')) n = n.nextElementSibling;
+    return n;
+  };
+  const hasChild = (() => {
+    const n = nextLi(li);
+    return n ? getDepth(n) > depth : false;
+  })();
+  if (!hasChild) return;
+  if (li.querySelector('.toc-toggle')) return;
+  const toggle = document.createElement('button');
+  toggle.type = 'button';
+  toggle.className = 'toc-toggle';
+  toggle.setAttribute('aria-expanded', 'true');
+  toggle.innerHTML = `
+            <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true" focusable="false">
+              <path d="M8.5 10.5 L12 14 L15.5 10.5" stroke="currentColor" stroke-width="1.6" fill="none"
+                    stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+          `;
+  const anchor = li.querySelector('a');
+  if (anchor) {
+    const wrapper = document.createElement('div');
+    wrapper.className = 'toc-row';
+    anchor.parentNode.insertBefore(wrapper, anchor);
+    wrapper.appendChild(anchor);
+    wrapper.appendChild(toggle);
+  } else {
+    li.appendChild(toggle);
+  }
+  li.dataset.collapsed = 'false';
   toggle.addEventListener('click', e => {
     e.preventDefault();
     const isCollapsed = li.dataset.collapsed === 'true';
-    ...
-    for (let k = idx + 1; k < items.length; k++) {
-      const childItem = items[k];
-      const childDepth = Number(childItem.dataset.depth);
-      if (childDepth <= depth) {
-        break;
+    const newState = String(!isCollapsed);
+    li.dataset.collapsed = newState;
+    toggle.setAttribute('aria-expanded', newState);
+    toggle.classList.toggle('rotated', !isCollapsed);
+    // Walk live DOM siblings to show/hide descendants
+    let cursor = nextLi(li);
+    while (cursor) {
+      const d = getDepth(cursor);
+      if (!Number.isFinite(d) || d <= depth) break;
+      if (!isCollapsed) {
+        cursor.style.display = 'none';
+      } else if (d === depth + 1) {
+        cursor.style.display = 'list-item';
       }
-      if (!isCollapsed) {
-        childItem.style.display = 'none';
-      } else {
-        if (childDepth === depth + 1) {
-          childItem.style.display = 'list-item';
-        }
+      // If parent remains collapsed, keep deeper levels hidden
+      if (isCollapsed && d > depth + 1) {
+        // no-op; deeper nodes will be revealed by their own parent toggles
       }
+      cursor = nextLi(cursor);
     }
   });
 });
Suggestion importance[1-10]: 7

__

Why: Correctly notes that relying on the static items array indices during click-time can desync with DOM changes and suggests walking live siblings, which is a reasonable robustness improvement. Impact is moderate since the current code likely works in most cases but could misbehave with dynamic mutations.

Medium
General
Sync initial ARIA and rotation state

The visual rotation and aria-expanded state are out of sync initially because
aria-expanded defaults to "true" while .rotated isn't applied. Initialize the
rotated class based on li.dataset.collapsed and keep them synchronized on creation
for correct accessibility and visuals.

toc.js [67-77]

+// After creating toggle and setting li.dataset.collapsed = 'false';
+const isInitiallyCollapsed = li.dataset.collapsed === 'true';
+toggle.setAttribute('aria-expanded', String(!isInitiallyCollapsed));
+toggle.classList.toggle('rotated', !isInitiallyCollapsed);
 toggle.addEventListener('click', e => {
   e.preventDefault();
   const isCollapsed = li.dataset.collapsed === 'true';
-  ...
-  toggle.setAttribute('aria-expanded', String(!isCollapsed));
+  const nextStateExpanded = String(isCollapsed);
+  li.dataset.collapsed = String(!isCollapsed);
+  toggle.setAttribute('aria-expanded', nextStateExpanded);
   toggle.classList.toggle('rotated', !isCollapsed);
-  ...
+  // ... rest of handler
 });
Suggestion importance[1-10]: 6

__

Why: It correctly identifies the initial mismatch between aria-expanded="true" and the absence of the .rotated class and proposes aligning them at creation. This improves accessibility and visual consistency, though it's a small, non-critical fix.

Low
Robust depth inference and clamping

Defaulting non-finite depth to 2 may mis-indent and break the collapse tree if real
depths vary. Clamp to a minimum of 0 and fall back to parsing the heading tag (e.g.,
h1..h6) to compute a reliable depth, ensuring consistent indentation and hierarchy.

toc.js [18-31]

 items.forEach(li => {
   let depthNum = Number(li.getAttribute('data-depth'));
   if (!Number.isFinite(depthNum)) {
-      depthNum = 2; // Fix for h4 headers producing NaN
+    // Fallback: infer from associated anchor/heading level if present
+    const heading = li.querySelector('a, [data-heading], [role="link"]');
+    const tag = heading && heading.tagName ? heading.tagName.toLowerCase() : '';
+    const match = tag.match(/^h([1-6])$/);
+    if (match) {
+      depthNum = Number(match[1]); // use heading level
+    } else {
+      depthNum = 1; // safe default minimal depth
+    }
   }
+  depthNum = Math.max(0, depthNum);
   li.dataset.depth = String(depthNum);
-  li.style.display = 'list-item'; // Ensure all items are visible initially
+  li.style.display = 'list-item';
 
   const a = li.querySelector('a');
   if (a) {
-      const indentStep = 1; // rem per depth level
-      a.style.marginLeft = `${depthNum * indentStep}rem`;
+    const indentStep = 1; // rem per depth level
+    a.style.marginLeft = `${depthNum * indentStep}rem`;
   }
 });
Suggestion importance[1-10]: 4

__

Why: The concern about defaulting NaN to 2 is plausible, but the proposed fallback to infer heading level from li descendants (like <a> tags) likely won't work in this DOM structure and may introduce inaccuracies. Minor potential benefit with some risk of being incorrect.

Low

@sharadregoti sharadregoti changed the title Fix TOC Add Collapsible/Expandable Side Nav (TOC) Oct 8, 2025
@sharadregoti sharadregoti merged commit 3faa79b into main Oct 8, 2025
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants