Skip to content

Commit 968b4e3

Browse files
committed
⚡️(frontend) enhance Table of Contents
- 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
1 parent dc2fe49 commit 968b4e3

File tree

3 files changed

+175
-141
lines changed

3 files changed

+175
-141
lines changed

src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import clsx from 'clsx';
22
import { useEffect } from 'react';
3-
import { css } from 'styled-components';
43

54
import { Box, Loading } from '@/components';
65
import { DocHeader } from '@/docs/doc-header/';
@@ -97,18 +96,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
9796

9897
return (
9998
<>
100-
{isDesktop && (
101-
<Box
102-
$height="100vh"
103-
$position="absolute"
104-
$css={css`
105-
top: 72px;
106-
right: 20px;
107-
`}
108-
>
109-
<TableContent />
110-
</Box>
111-
)}
99+
{isDesktop && <TableContent />}
112100
<DocEditorContainer
113101
docHeader={<DocHeader doc={doc} />}
114102
docEditor={

src/frontend/apps/impress/src/features/docs/doc-editor/hook/useHeadings.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,33 @@ export const useHeadings = (editor: DocsBlockNoteEditor) => {
99
useEffect(() => {
1010
setHeadings(editor);
1111

12-
const unsubscribe = editor?.onChange(() => {
13-
setHeadings(editor);
12+
let timeoutId: NodeJS.Timeout;
13+
14+
const unsubscribe = editor?.onChange((_, context) => {
15+
clearTimeout(timeoutId);
16+
17+
timeoutId = setTimeout(() => {
18+
const blocksChanges = context.getChanges();
19+
20+
if (!blocksChanges.length) {
21+
return;
22+
}
23+
24+
const blockChanges = blocksChanges[0];
25+
26+
if (
27+
blockChanges.type !== 'update' ||
28+
blockChanges.block.type !== 'heading'
29+
) {
30+
return;
31+
}
32+
33+
setHeadings(editor);
34+
}, 500);
1435
});
1536

1637
return () => {
38+
clearTimeout(timeoutId);
1739
resetHeadings();
1840
unsubscribe();
1941
};

src/frontend/apps/impress/src/features/docs/doc-table-content/components/TableContent.tsx

Lines changed: 150 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,112 @@ import { MAIN_LAYOUT_ID } from '@/layouts/conf';
1010
import { Heading } from './Heading';
1111

1212
export const TableContent = () => {
13+
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
14+
const [containerHeight, setContainerHeight] = useState('100vh');
15+
16+
const { t } = useTranslation();
17+
const [isOpen, setIsOpen] = useState(false);
18+
19+
/**
20+
* Calculate container height based on the scrollable content
21+
*/
22+
useEffect(() => {
23+
const mainLayout = document.getElementById(MAIN_LAYOUT_ID);
24+
if (mainLayout) {
25+
setContainerHeight(`${mainLayout.scrollHeight}px`);
26+
}
27+
}, []);
28+
29+
const onOpen = () => {
30+
setIsOpen(true);
31+
};
32+
33+
return (
34+
<Box
35+
$height={containerHeight}
36+
$position="absolute"
37+
$css={css`
38+
top: 72px;
39+
right: 20px;
40+
`}
41+
>
42+
<Box
43+
as="nav"
44+
id="summaryContainer"
45+
$width={!isOpen ? '40px' : '200px'}
46+
$height={!isOpen ? '40px' : 'auto'}
47+
$maxHeight="calc(50vh - 60px)"
48+
$zIndex={1000}
49+
$align="center"
50+
$padding={isOpen ? 'xs' : '0'}
51+
$justify="center"
52+
$position="sticky"
53+
aria-label={t('Summary')}
54+
$css={css`
55+
top: var(--c--globals--spacings--0);
56+
border: 1px solid ${colorsTokens['brand-100']};
57+
overflow: hidden;
58+
border-radius: ${spacingsTokens['3xs']};
59+
background: ${colorsTokens['gray-000']};
60+
${isOpen &&
61+
css`
62+
display: flex;
63+
flex-direction: column;
64+
justify-content: flex-start;
65+
align-items: flex-start;
66+
gap: ${spacingsTokens['2xs']};
67+
`}
68+
`}
69+
className="--docs--table-content"
70+
>
71+
{!isOpen && (
72+
<BoxButton
73+
onClick={onOpen}
74+
$width="100%"
75+
$height="100%"
76+
$justify="center"
77+
$align="center"
78+
aria-label={t('Summary')}
79+
aria-expanded={isOpen}
80+
aria-controls="toc-list"
81+
$css={css`
82+
&:focus-visible {
83+
outline: none;
84+
box-shadow: 0 0 0 4px ${colorsTokens['brand-400']};
85+
background: ${colorsTokens['brand-100']};
86+
width: 90%;
87+
height: 90%;
88+
}
89+
`}
90+
>
91+
<Icon
92+
$theme="brand"
93+
$variation="tertiary"
94+
iconName="list"
95+
variant="symbols-outlined"
96+
/>
97+
</BoxButton>
98+
)}
99+
{isOpen && <TableContentOpened setIsOpen={setIsOpen} />}
100+
</Box>
101+
</Box>
102+
);
103+
};
104+
105+
const TableContentOpened = ({
106+
setIsOpen,
107+
}: {
108+
setIsOpen: (isOpen: boolean) => void;
109+
}) => {
13110
const { headings } = useHeadingStore();
14111
const { editor } = useEditorStore();
15112
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
16-
17113
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
18-
19114
const { t } = useTranslation();
20-
const [isHover, setIsHover] = useState(false);
21115

116+
/**
117+
* Handle scroll to highlight the current heading in the table of content
118+
*/
22119
useEffect(() => {
23120
const handleScroll = () => {
24121
if (!headings) {
@@ -69,23 +166,10 @@ export const TableContent = () => {
69166
.getElementById(MAIN_LAYOUT_ID)
70167
?.removeEventListener('scroll', scrollFn);
71168
};
72-
}, [headings, setHeadingIdHighlight]);
73-
74-
const onOpen = () => {
75-
setIsHover(true);
76-
setTimeout(() => {
77-
const element = document.getElementById(`heading-${headingIdHighlight}`);
78-
79-
element?.scrollIntoView({
80-
behavior: 'instant',
81-
inline: 'center',
82-
block: 'center',
83-
});
84-
}, 0); // 300ms is the transition time of the box
85-
};
169+
}, [headings]);
86170

87171
const onClose = () => {
88-
setIsHover(false);
172+
setIsOpen(false);
89173
};
90174

91175
if (
@@ -99,129 +183,69 @@ export const TableContent = () => {
99183

100184
return (
101185
<Box
102-
as="nav"
103-
id="summaryContainer"
104-
$width={!isHover ? '40px' : '200px'}
105-
$height={!isHover ? '40px' : 'auto'}
106-
$maxHeight="calc(50vh - 60px)"
107-
$zIndex={1000}
108-
$align="center"
109-
$padding={isHover ? 'xs' : '0'}
110-
$justify="center"
111-
$position="sticky"
112-
aria-label={t('Summary')}
186+
$width="100%"
187+
$overflow="hidden"
113188
$css={css`
114-
top: var(--c--globals--spacings--0);
115-
border: 1px solid ${colorsTokens['brand-100']};
116-
overflow: hidden;
117-
border-radius: ${spacingsTokens['3xs']};
118-
background: ${colorsTokens['gray-000']};
119-
${isHover &&
120-
css`
121-
display: flex;
122-
flex-direction: column;
123-
justify-content: flex-start;
124-
align-items: flex-start;
125-
gap: ${spacingsTokens['2xs']};
126-
`}
189+
user-select: none;
190+
padding: ${spacingsTokens['4xs']};
127191
`}
128-
className="--docs--table-content"
129192
>
130-
{!isHover && (
193+
<Box
194+
$margin={{ bottom: spacingsTokens.xs }}
195+
$direction="row"
196+
$justify="space-between"
197+
$align="center"
198+
>
199+
<Text $weight="500" $size="sm">
200+
{t('Summary')}
201+
</Text>
131202
<BoxButton
132-
onClick={onOpen}
133-
$width="100%"
134-
$height="100%"
203+
onClick={onClose}
135204
$justify="center"
136205
$align="center"
137206
aria-label={t('Summary')}
138-
aria-expanded={isHover}
207+
aria-expanded="true"
139208
aria-controls="toc-list"
140209
$css={css`
210+
transition: none !important;
211+
transform: rotate(180deg);
141212
&:focus-visible {
142213
outline: none;
143-
box-shadow: 0 0 0 4px ${colorsTokens['brand-400']};
144-
background: ${colorsTokens['brand-100']};
145-
width: 90%;
146-
height: 90%;
214+
box-shadow: 0 0 0 2px ${colorsTokens['brand-400']};
215+
border-radius: var(--c--globals--spacings--st);
147216
}
148217
`}
149218
>
150-
<Icon
151-
$theme="brand"
152-
$variation="tertiary"
153-
iconName="list"
154-
variant="symbols-outlined"
155-
/>
219+
<Icon iconName="menu_open" $theme="brand" $variation="tertiary" />
156220
</BoxButton>
157-
)}
158-
{isHover && (
159-
<Box
160-
$width="100%"
161-
$overflow="hidden"
162-
$css={css`
163-
user-select: none;
164-
padding: ${spacingsTokens['4xs']};
165-
`}
166-
>
167-
<Box
168-
$margin={{ bottom: spacingsTokens.xs }}
169-
$direction="row"
170-
$justify="space-between"
171-
$align="center"
172-
>
173-
<Text $weight="500" $size="sm">
174-
{t('Summary')}
175-
</Text>
176-
<BoxButton
177-
onClick={onClose}
178-
$justify="center"
179-
$align="center"
180-
aria-label={t('Summary')}
181-
aria-expanded={isHover}
182-
aria-controls="toc-list"
183-
$css={css`
184-
transition: none !important;
185-
transform: rotate(180deg);
186-
&:focus-visible {
187-
outline: none;
188-
box-shadow: 0 0 0 2px ${colorsTokens['brand-400']};
189-
border-radius: var(--c--globals--spacings--st);
190-
}
191-
`}
192-
>
193-
<Icon iconName="menu_open" $theme="brand" $variation="tertiary" />
194-
</BoxButton>
195-
</Box>
196-
<Box
197-
as="ul"
198-
id="toc-list"
199-
role="list"
200-
$gap={spacingsTokens['3xs']}
201-
$css={css`
202-
overflow-y: auto;
203-
list-style: none;
204-
padding: ${spacingsTokens['3xs']};
205-
margin: 0;
206-
`}
207-
>
208-
{headings?.map(
209-
(heading) =>
210-
heading.contentText && (
211-
<Box as="li" role="listitem" key={heading.id}>
212-
<Heading
213-
editor={editor}
214-
headingId={heading.id}
215-
level={heading.props.level}
216-
text={heading.contentText}
217-
isHighlight={headingIdHighlight === heading.id}
218-
/>
219-
</Box>
220-
),
221-
)}
222-
</Box>
223-
</Box>
224-
)}
221+
</Box>
222+
<Box
223+
as="ul"
224+
id="toc-list"
225+
role="list"
226+
$gap={spacingsTokens['3xs']}
227+
$css={css`
228+
overflow-y: auto;
229+
list-style: none;
230+
padding: ${spacingsTokens['3xs']};
231+
margin: 0;
232+
`}
233+
>
234+
{headings?.map(
235+
(heading) =>
236+
heading.contentText && (
237+
<Box as="li" role="listitem" key={heading.id}>
238+
<Heading
239+
editor={editor}
240+
headingId={heading.id}
241+
level={heading.props.level}
242+
text={heading.contentText}
243+
isHighlight={headingIdHighlight === heading.id}
244+
/>
245+
</Box>
246+
),
247+
)}
248+
</Box>
225249
</Box>
226250
);
227251
};

0 commit comments

Comments
 (0)