Skip to content

Commit bc42bf6

Browse files
authored
Merge branch 'main' into conico/img-preload
2 parents c077b18 + 6b588af commit bc42bf6

File tree

8 files changed

+111
-37
lines changed

8 files changed

+111
-37
lines changed

.changeset/quiet-icons-deny.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'gitbook': patch
3+
---
4+
5+
Fix ToggeableLinkItem display

.github/CONTRIBUTING.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,9 @@ After forking this repository, you'll want to [create a branch](https://docs.git
5454
#### 3. Install dependencies and run the project locally
5555

5656
##### Prerequisites:
57-
- Node.js (Version: >= 22.3)
58-
- Use `nvm` for easy Node management
59-
- [Bun](https://bun.sh/) (Version: >=1.2.15)
60-
- We use a text-based lockfile which isn't supported below 1.2.15
57+
58+
- [Node.js](https://nodejs.org/en) (see "engines" in `package.json`)
59+
- [Bun](https://bun.sh/) (see "packageManager" in `package.json`)
6160

6261
##### Setup steps:
6362

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
"name": "gitbook",
33
"version": "0.1.0",
4+
"engines": {
5+
"node": "^22.3.0"
6+
},
47
"devDependencies": {
58
"@biomejs/biome": "^1.9.4",
69
"@changesets/cli": "^2.29.7",

packages/gitbook/e2e/internal.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,58 @@ const testCases: TestsCase[] = [
228228
);
229229
},
230230
},
231+
{
232+
name: 'Expandable TOC navigation',
233+
url: '',
234+
run: async (page) => {
235+
await waitForCookiesDialog(page);
236+
237+
// Verify "Navigation" link is not visible initially
238+
const navigationLink = page.getByRole('link', { name: 'Navigation' });
239+
await expect(navigationLink).not.toBeVisible();
240+
241+
// Find and click the chevron element that is next to "Editor" in the TOC
242+
// It is a span inside the link
243+
const editorChevron = page
244+
.getByRole('link', { name: 'Editor' })
245+
.locator('span');
246+
await editorChevron.click();
247+
248+
// Verify "Navigation" link becomes visible after expansion
249+
await expect(navigationLink).toBeVisible();
250+
},
251+
},
252+
{
253+
name: 'Expandable nested TOC navigation',
254+
url: '',
255+
screenshot: false,
256+
run: async (page) => {
257+
await waitForCookiesDialog(page);
258+
259+
// Verify "Spaces" link is not visible initially
260+
const navigationLink = page.getByRole('link', { name: 'Spaces' });
261+
await expect(navigationLink).not.toBeVisible();
262+
263+
// Find and click the chevron element that is next to "Editor" in the TOC
264+
// It is a span inside the link
265+
const editorChevron = page
266+
.getByRole('link', { name: 'Editor' })
267+
.locator('span');
268+
await editorChevron.click();
269+
270+
// At this stage the link should still not be visible
271+
await expect(navigationLink).not.toBeVisible();
272+
273+
// Then we click 'Content Structure' chevron to expand further
274+
const contentStructureChevron = page
275+
.getByRole('link', { name: 'Content Structure' })
276+
.locator('span');
277+
await contentStructureChevron.click();
278+
279+
// Verify "Spaces" link becomes visible after expansion
280+
await expect(navigationLink).toBeVisible();
281+
},
282+
},
231283
...searchTestCases,
232284
{
233285
name: 'Not found',

packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@
22

33
import React, { memo, useCallback, useMemo, type ComponentPropsWithRef } from 'react';
44

5-
import {
6-
NavigationStatusContext,
7-
useHash,
8-
useIsMounted,
9-
useListOverflow,
10-
} from '@/components/hooks';
5+
import { useHash, useIsMounted, useListOverflow } from '@/components/hooks';
116
import { DropdownMenu, DropdownMenuItem } from '@/components/primitives';
127
import { useLanguage } from '@/intl/client';
138
import { tString } from '@/intl/translate';
@@ -74,7 +69,6 @@ export function DynamicTabs(props: {
7469
}) {
7570
const { id, tabs, className } = props;
7671
const router = useRouter();
77-
const { onNavigationClick } = React.useContext(NavigationStatusContext);
7872

7973
const hash = useHash();
8074
const [tabsState, setTabsState] = useTabsState();
@@ -106,8 +100,7 @@ export function DynamicTabs(props: {
106100

107101
const href = `#${tab.id}`;
108102
if (window.location.hash !== href) {
109-
router.replace(href);
110-
onNavigationClick(href);
103+
router.replace(href, { scroll: false });
111104
}
112105

113106
setTabsState((prev) => {
@@ -128,7 +121,7 @@ export function DynamicTabs(props: {
128121
};
129122
});
130123
},
131-
[onNavigationClick, router, setTabsState, tabs, id]
124+
[router, setTabsState, tabs, id]
132125
);
133126

134127
// When the hash changes, we try to select the tab containing the targetted element.
@@ -184,7 +177,10 @@ const TabPanel = memo(function TabPanel(props: {
184177
role="tabpanel"
185178
id={tab.id}
186179
aria-labelledby={getTabButtonId(tab.id)}
187-
className={tcls('p-4', isActive ? null : 'hidden')}
180+
className={tcls(
181+
'scroll-mt-[calc(var(--content-scroll-margin)+var(--spacing)*12)] p-4',
182+
isActive ? null : 'hidden'
183+
)}
188184
>
189185
{tab.body}
190186
</div>

packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { Icon } from '@gitbook/icons';
4-
import { motion } from 'motion/react';
4+
import { AnimatePresence, motion } from 'motion/react';
55
import React, { useRef } from 'react';
66

77
import { tcls } from '@/lib/tailwind';
@@ -180,14 +180,10 @@ function Toggler(props: {
180180
const show = {
181181
opacity: 1,
182182
height: 'auto',
183-
display: 'inherit',
184183
};
185184
const hide = {
186185
opacity: 0,
187186
height: 0,
188-
transitionEnd: {
189-
display: 'none',
190-
},
191187
};
192188

193189
function Descendants(props: {
@@ -196,12 +192,17 @@ function Descendants(props: {
196192
}) {
197193
const { isVisible, children } = props;
198194
return (
199-
<motion.div
200-
animate={isVisible ? show : hide}
201-
initial={isVisible ? show : hide}
202-
style={{ overflow: 'hidden' }}
203-
>
204-
{children}
205-
</motion.div>
195+
<AnimatePresence>
196+
{isVisible ? (
197+
<motion.div
198+
initial={hide}
199+
animate={show}
200+
exit={hide}
201+
className="flex flex-col overflow-hidden"
202+
>
203+
{children}
204+
</motion.div>
205+
) : null}
206+
</AnimatePresence>
206207
);
207208
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as React from 'react';
2+
3+
/**
4+
* Returns the value of the previous render.
5+
*/
6+
export function usePrevious<T>(value: T): T | undefined {
7+
const ref = React.useRef<T | undefined>(undefined);
8+
React.useLayoutEffect(() => {
9+
ref.current = value;
10+
});
11+
return ref.current;
12+
}

packages/gitbook/src/components/hooks/useScrollPage.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { usePathname } from 'next/navigation';
44
import React from 'react';
55

66
import { useHash } from './useHash';
7+
import { usePrevious } from './usePrevious';
78

89
/**
910
* Scroll the page to an anchor point or
@@ -12,8 +13,17 @@ import { useHash } from './useHash';
1213
*/
1314
export function useScrollPage() {
1415
const hash = useHash();
16+
const previousHash = usePrevious(hash);
1517
const pathname = usePathname();
18+
const previousPathname = usePrevious(pathname);
1619
React.useLayoutEffect(() => {
20+
// If there is no change in pathname or hash, do nothing
21+
if (previousHash === hash && previousPathname === pathname) {
22+
return;
23+
}
24+
25+
// If there is a hash
26+
// - Triggered by a change of hash or pathname
1727
if (hash) {
1828
const element = document.getElementById(hash);
1929
if (element) {
@@ -22,16 +32,12 @@ export function useScrollPage() {
2232
behavior: 'smooth',
2333
});
2434
}
25-
} else {
35+
return;
36+
}
37+
38+
// If there was a hash but not anymore, scroll to top
39+
if (previousHash && !hash) {
2640
window.scrollTo(0, 0);
2741
}
28-
return () => {
29-
if (hash) {
30-
const element = document.getElementById(hash);
31-
if (element) {
32-
element.style.scrollMarginTop = '';
33-
}
34-
}
35-
};
36-
}, [hash, pathname]);
42+
}, [hash, previousHash, pathname, previousPathname]);
3743
}

0 commit comments

Comments
 (0)