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:
@ -1,13 +1,13 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
export function useHeadsObserver(location: string) {
|
export function useHeadsObserver(location: string) {
|
||||||
const [activeText, setActiveText] = useState('');
|
const [activeId, setActiveId] = useState('');
|
||||||
const observer = useRef<IntersectionObserver | null>(null);
|
const observer = useRef<IntersectionObserver | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleObsever = (entries: any[]) => {
|
const handleObsever = (entries: any[]) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry?.isIntersecting) {
|
if (entry?.isIntersecting) {
|
||||||
setActiveText(entry.target.innerText);
|
setActiveId(entry.target.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -21,5 +21,5 @@ export function useHeadsObserver(location: string) {
|
|||||||
return () => observer.current?.disconnect();
|
return () => observer.current?.disconnect();
|
||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
return { activeText };
|
return { activeId };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,7 +92,7 @@ interface HeadingType {
|
|||||||
const DocsTableContents = () => {
|
const DocsTableContents = () => {
|
||||||
const [headings, setHeadings] = useState<HeadingType[]>([]);
|
const [headings, setHeadings] = useState<HeadingType[]>([]);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { activeText } = useHeadsObserver(pathname);
|
const { activeId } = useHeadsObserver(pathname);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nodes: HTMLElement[] = Array.from(
|
const nodes: HTMLElement[] = Array.from(
|
||||||
@ -126,11 +126,11 @@ const DocsTableContents = () => {
|
|||||||
<StyledUnorderedList>
|
<StyledUnorderedList>
|
||||||
{headings.map((heading) => (
|
{headings.map((heading) => (
|
||||||
<StyledList
|
<StyledList
|
||||||
key={heading.text}
|
key={heading.id}
|
||||||
style={getStyledHeading(heading.level)}
|
style={getStyledHeading(heading.level)}
|
||||||
>
|
>
|
||||||
<StyledLink
|
<StyledLink
|
||||||
href={`#${heading.text}`}
|
href={`#${heading.id}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const yOffset = -70;
|
const yOffset = -70;
|
||||||
@ -142,8 +142,7 @@ const DocsTableContents = () => {
|
|||||||
window.scrollTo({ top: y, behavior: 'smooth' });
|
window.scrollTo({ top: y, behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
fontWeight:
|
fontWeight: activeId === heading.id ? 'bold' : 'normal',
|
||||||
activeText === heading.text ? 'bold' : 'normal',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{heading.text}
|
{heading.text}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, {
|
import {
|
||||||
Children,
|
Children,
|
||||||
cloneElement,
|
cloneElement,
|
||||||
isValidElement,
|
isValidElement,
|
||||||
@ -12,6 +12,7 @@ export const wrapHeadingsWithAnchor = (children: ReactNode): ReactNode => {
|
|||||||
): element is ReactElement<{ children: ReactNode }> => {
|
): element is ReactElement<{ children: ReactNode }> => {
|
||||||
return element.props.children !== undefined;
|
return element.props.children !== undefined;
|
||||||
};
|
};
|
||||||
|
const idCounts = new Map<string, number>();
|
||||||
|
|
||||||
return Children.map(children, (child) => {
|
return Children.map(children, (child) => {
|
||||||
if (
|
if (
|
||||||
@ -19,10 +20,15 @@ export const wrapHeadingsWithAnchor = (children: ReactNode): ReactNode => {
|
|||||||
typeof child.type === 'string' &&
|
typeof child.type === 'string' &&
|
||||||
['h1', 'h2', 'h3', 'h4'].includes(child.type)
|
['h1', 'h2', 'h3', 'h4'].includes(child.type)
|
||||||
) {
|
) {
|
||||||
const id = child.props.children
|
const baseId = child.props.children
|
||||||
.toString()
|
.toString()
|
||||||
.replace(/\s+/g, '-')
|
.replace(/\s+/g, '-')
|
||||||
.toLowerCase();
|
.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>, {
|
return cloneElement(child as ReactElement<any>, {
|
||||||
id,
|
id,
|
||||||
className: 'anchor',
|
className: 'anchor',
|
||||||
|
|||||||
Reference in New Issue
Block a user