Skip to content

Commit a2fb627

Browse files
authored
Fix scroll jump and scroll reset (#3756)
1 parent f9f8011 commit a2fb627

File tree

3 files changed

+61
-42
lines changed

3 files changed

+61
-42
lines changed

packages/gitbook/src/components/SitePage/SitePage.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
} from '@gitbook/api';
88
import type { Metadata, Viewport } from 'next';
99
import { notFound, redirect } from 'next/navigation';
10-
import React from 'react';
1110

1211
import { PageAside } from '@/components/PageAside';
1312
import { PageBody, PageCover } from '@/components/PageBody';
@@ -75,9 +74,7 @@ export async function SitePage(props: SitePageProps) {
7574
insightsDisplayContext={SiteInsightsDisplayContext.Site}
7675
/>
7776
</div>
78-
<React.Suspense fallback={null}>
79-
<PageClientLayout />
80-
</React.Suspense>
77+
<PageClientLayout />
8178
</div>
8279
</PageContextProvider>
8380
);

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@ import { useHash } from './useHash';
77
import { usePrevious } from './usePrevious';
88

99
/**
10-
* Scroll the page to an anchor point or
11-
* to the top of the page when navigating between pages (pathname)
12-
* or sections of a page (hash).
10+
* Scroll the page to the hash or reset scroll to the top.
11+
* Only triggered while navigating in the app, not for initial load.
1312
*/
1413
export function useScrollPage() {
1514
const hash = useHash();
1615
const previousHash = usePrevious(hash);
1716
const pathname = usePathname();
1817
const previousPathname = usePrevious(pathname);
1918
React.useLayoutEffect(() => {
19+
if (!previousHash && !previousPathname) {
20+
return;
21+
}
22+
2023
// If there is no change in pathname or hash, do nothing
2124
if (previousHash === hash && previousPathname === pathname) {
2225
return;
@@ -31,13 +34,10 @@ export function useScrollPage() {
3134
block: 'start',
3235
behavior: 'smooth',
3336
});
37+
return;
3438
}
35-
return;
3639
}
3740

38-
// If there was a hash but not anymore, scroll to top
39-
if (previousHash && !hash) {
40-
window.scrollTo(0, 0);
41-
}
41+
window.scrollTo(0, 0);
4242
}, [hash, previousHash, pathname, previousPathname]);
4343
}

packages/gitbook/src/components/primitives/ScrollContainer.tsx

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { tString, useLanguage } from '@/intl/client';
44
import { tcls } from '@/lib/tailwind';
55
import * as React from 'react';
6+
import { useScrollListener } from '../hooks/useScrollListener';
67
import { Button } from './Button';
78

89
/**
@@ -46,63 +47,61 @@ export function ScrollContainer(props: ScrollContainerProps) {
4647

4748
const language = useLanguage();
4849

49-
React.useEffect(() => {
50+
useScrollListener(() => {
5051
const container = containerRef.current;
5152
if (!container) {
5253
return;
5354
}
5455

55-
// Update scroll position on scroll using requestAnimationFrame
56-
const scrollListener: EventListener = () => {
57-
requestAnimationFrame(() => {
58-
setScrollPosition(
59-
orientation === 'horizontal' ? container.scrollLeft : container.scrollTop
60-
);
61-
});
62-
};
63-
container.addEventListener('scroll', scrollListener);
56+
setScrollPosition(
57+
orientation === 'horizontal' ? container.scrollLeft : container.scrollTop
58+
);
59+
}, containerRef);
60+
61+
React.useEffect(() => {
62+
const container = containerRef.current;
63+
if (!container) {
64+
return;
65+
}
6466

6567
// Update max scroll position using resize observer
66-
const resizeObserver = new ResizeObserver((entries) => {
67-
const containerEntry = entries.find((i) => i.target === containerRef.current);
68-
if (containerEntry) {
68+
const ro = new ResizeObserver((entries) => {
69+
const [entry] = entries;
70+
if (entry) {
6971
setScrollSize(
7072
orientation === 'horizontal'
71-
? containerEntry.target.scrollWidth - containerEntry.target.clientWidth - 1
72-
: containerEntry.target.scrollHeight -
73-
containerEntry.target.clientHeight -
74-
1
73+
? entry.target.scrollWidth - entry.target.clientWidth - 1
74+
: entry.target.scrollHeight - entry.target.clientHeight - 1
7575
);
7676
}
7777
});
78-
resizeObserver.observe(container);
7978

80-
return () => {
81-
container.removeEventListener('scroll', scrollListener);
82-
resizeObserver.disconnect();
83-
};
79+
ro.observe(container);
80+
81+
return () => ro.disconnect();
8482
}, [orientation]);
8583

86-
// Scroll to the active item
8784
React.useEffect(() => {
8885
const container = containerRef.current;
89-
if (!container || !activeId) {
86+
if (!container) {
9087
return;
9188
}
92-
const activeItem = container.querySelector(`#${CSS.escape(activeId)}`);
93-
if (activeItem) {
94-
activeItem.scrollIntoView({
95-
inline: 'center',
96-
block: 'center',
97-
});
89+
if (!activeId) {
90+
return;
91+
}
92+
const activeItem = document.getElementById(activeId);
93+
if (!activeItem || !container.contains(activeItem)) {
94+
return;
9895
}
96+
scrollToElementInContainer(activeItem, container);
9997
}, [activeId]);
10098

10199
const scrollFurther = () => {
102100
const container = containerRef.current;
103101
if (!container) {
104102
return;
105103
}
104+
106105
container.scrollTo({
107106
top: orientation === 'vertical' ? scrollPosition + container.clientHeight : undefined,
108107
left: orientation === 'horizontal' ? scrollPosition + container.clientWidth : undefined,
@@ -115,6 +114,7 @@ export function ScrollContainer(props: ScrollContainerProps) {
115114
if (!container) {
116115
return;
117116
}
117+
118118
container.scrollTo({
119119
top: orientation === 'vertical' ? scrollPosition - container.clientHeight : undefined,
120120
left: orientation === 'horizontal' ? scrollPosition - container.clientWidth : undefined,
@@ -194,3 +194,25 @@ export function ScrollContainer(props: ScrollContainerProps) {
194194
</div>
195195
);
196196
}
197+
198+
/**
199+
* Scroll to an element in a container.
200+
*/
201+
function scrollToElementInContainer(element: HTMLElement, container: HTMLElement) {
202+
const containerRect = container.getBoundingClientRect();
203+
const rect = element.getBoundingClientRect();
204+
205+
return container.scrollTo({
206+
top:
207+
container.scrollTop +
208+
(rect.top - containerRect.top) -
209+
container.clientHeight / 2 +
210+
rect.height / 2,
211+
left:
212+
container.scrollLeft +
213+
(rect.left - containerRect.left) -
214+
container.clientWidth / 2 +
215+
rect.width / 2,
216+
behavior: 'smooth',
217+
});
218+
}

0 commit comments

Comments
 (0)