From f7d4e6810bcd6128dc08f9b548146c8ec1ec44bb Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 27 Nov 2025 17:41:22 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F(frontend)=20enhance=20Table?= =?UTF-8?q?=20of=20Contents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the Table of Contents stickiness now covers the full height of the viewport, before it was limited to 100vh - we listen the scroll to highlight the heading in the Table of Contents only when the Table of Contents is open - We debounce the editor change to avoid excessive updates to the Table of Contents --- .../docs/doc-editor/components/DocEditor.tsx | 14 +- .../docs/doc-editor/hook/useHeadings.tsx | 26 +- .../components/TableContent.tsx | 276 ++++++++++-------- 3 files changed, 175 insertions(+), 141 deletions(-) diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx index 1562070e..d8070664 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx @@ -1,6 +1,5 @@ import clsx from 'clsx'; import { useEffect } from 'react'; -import { css } from 'styled-components'; import { Box, Loading } from '@/components'; import { DocHeader } from '@/docs/doc-header/'; @@ -97,18 +96,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => { return ( <> - {isDesktop && ( - - - - )} + {isDesktop && } } docEditor={ diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx index 6d89a8aa..a84626c3 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx @@ -9,11 +9,33 @@ export const useHeadings = (editor: DocsBlockNoteEditor) => { useEffect(() => { setHeadings(editor); - const unsubscribe = editor?.onChange(() => { - setHeadings(editor); + let timeoutId: NodeJS.Timeout; + const DEBOUNCE_DELAY = 500; + const unsubscribe = editor?.onChange((_, context) => { + clearTimeout(timeoutId); + + timeoutId = setTimeout(() => { + const blocksChanges = context.getChanges(); + + if (!blocksChanges.length) { + return; + } + + const blockChanges = blocksChanges[0]; + + if ( + blockChanges.type !== 'update' || + blockChanges.block.type !== 'heading' + ) { + return; + } + + setHeadings(editor); + }, DEBOUNCE_DELAY); }); return () => { + clearTimeout(timeoutId); resetHeadings(); unsubscribe(); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx index 8fdd946e..febdf0ab 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx @@ -10,15 +10,112 @@ import { MAIN_LAYOUT_ID } from '@/layouts/conf'; import { Heading } from './Heading'; export const TableContent = () => { + const { spacingsTokens, colorsTokens } = useCunninghamTheme(); + const [containerHeight, setContainerHeight] = useState('100vh'); + + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + + /** + * Calculate container height based on the scrollable content + */ + useEffect(() => { + const mainLayout = document.getElementById(MAIN_LAYOUT_ID); + if (mainLayout) { + setContainerHeight(`${mainLayout.scrollHeight}px`); + } + }, []); + + const onOpen = () => { + setIsOpen(true); + }; + + return ( + + + {!isOpen && ( + + + + )} + {isOpen && } + + + ); +}; + +const TableContentOpened = ({ + setIsOpen, +}: { + setIsOpen: (isOpen: boolean) => void; +}) => { const { headings } = useHeadingStore(); const { editor } = useEditorStore(); const { spacingsTokens, colorsTokens } = useCunninghamTheme(); - const [headingIdHighlight, setHeadingIdHighlight] = useState(); - const { t } = useTranslation(); - const [isHover, setIsHover] = useState(false); + /** + * Handle scroll to highlight the current heading in the table of content + */ useEffect(() => { const handleScroll = () => { if (!headings) { @@ -69,23 +166,10 @@ export const TableContent = () => { .getElementById(MAIN_LAYOUT_ID) ?.removeEventListener('scroll', scrollFn); }; - }, [headings, setHeadingIdHighlight]); - - const onOpen = () => { - setIsHover(true); - setTimeout(() => { - const element = document.getElementById(`heading-${headingIdHighlight}`); - - element?.scrollIntoView({ - behavior: 'instant', - inline: 'center', - block: 'center', - }); - }, 0); // 300ms is the transition time of the box - }; + }, [headings]); const onClose = () => { - setIsHover(false); + setIsOpen(false); }; if ( @@ -99,129 +183,69 @@ export const TableContent = () => { return ( - {!isHover && ( + + + {t('Summary')} + - + - )} - {isHover && ( - - - - {t('Summary')} - - - - - - - {headings?.map( - (heading) => - heading.contentText && ( - - - - ), - )} - - - )} + + + {headings?.map( + (heading) => + heading.contentText && ( + + + + ), + )} + ); };