Fix: Twenty-website docs same TOC ids #11865 (#11872)

This PR fixes issue https://github.com/twentyhq/twenty/issues/11865.

The highlight heading logic in TOC was checking the heading text which
could be the same for multiple headings.
The ids for these headings were also just the heading texts, leading to
conflict in ids too.

Fix:
- Appended index of the heading item from the list of headings to the id
of the heading. This fixed conflicting ids.
- Used these unique ids to toggle the highlight style.

Behaviour after the fix:


https://github.com/user-attachments/assets/ab3bc205-0b0e-451d-b9cb-4fa852263efc


Edit:
close #11865

---------

Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
Prajwal Dhule
2025-05-06 18:04:52 +05:30
committed by GitHub
parent 6810ca0204
commit f1d658bcb6
3 changed files with 15 additions and 10 deletions

View File

@ -1,13 +1,13 @@
import { useEffect, useRef, useState } from 'react';
export function useHeadsObserver(location: string) {
const [activeText, setActiveText] = useState('');
const [activeId, setActiveId] = useState('');
const observer = useRef<IntersectionObserver | null>(null);
useEffect(() => {
const handleObsever = (entries: any[]) => {
entries.forEach((entry) => {
if (entry?.isIntersecting) {
setActiveText(entry.target.innerText);
setActiveId(entry.target.id);
}
});
};
@ -21,5 +21,5 @@ export function useHeadsObserver(location: string) {
return () => observer.current?.disconnect();
}, [location]);
return { activeText };
return { activeId };
}

View File

@ -92,7 +92,7 @@ interface HeadingType {
const DocsTableContents = () => {
const [headings, setHeadings] = useState<HeadingType[]>([]);
const pathname = usePathname();
const { activeText } = useHeadsObserver(pathname);
const { activeId } = useHeadsObserver(pathname);
useEffect(() => {
const nodes: HTMLElement[] = Array.from(
@ -126,11 +126,11 @@ const DocsTableContents = () => {
<StyledUnorderedList>
{headings.map((heading) => (
<StyledList
key={heading.text}
key={heading.id}
style={getStyledHeading(heading.level)}
>
<StyledLink
href={`#${heading.text}`}
href={`#${heading.id}`}
onClick={(e) => {
e.preventDefault();
const yOffset = -70;
@ -142,8 +142,7 @@ const DocsTableContents = () => {
window.scrollTo({ top: y, behavior: 'smooth' });
}}
style={{
fontWeight:
activeText === heading.text ? 'bold' : 'normal',
fontWeight: activeId === heading.id ? 'bold' : 'normal',
}}
>
{heading.text}

View File

@ -1,4 +1,4 @@
import React, {
import {
Children,
cloneElement,
isValidElement,
@ -12,6 +12,7 @@ export const wrapHeadingsWithAnchor = (children: ReactNode): ReactNode => {
): element is ReactElement<{ children: ReactNode }> => {
return element.props.children !== undefined;
};
const idCounts = new Map<string, number>();
return Children.map(children, (child) => {
if (
@ -19,10 +20,15 @@ export const wrapHeadingsWithAnchor = (children: ReactNode): ReactNode => {
typeof child.type === 'string' &&
['h1', 'h2', 'h3', 'h4'].includes(child.type)
) {
const id = child.props.children
const baseId = child.props.children
.toString()
.replace(/\s+/g, '-')
.toLowerCase();
const idCount = idCounts.get(baseId) ?? 0;
const id = idCount === 0 ? baseId : `${baseId}-${idCount}`;
idCounts.set(baseId, idCount + 1);
return cloneElement(child as ReactElement<any>, {
id,
className: 'anchor',