Skip to content

Commit 5af5d29

Browse files
authored
fix(platform): Automatically scroll to search hits on same page (#11700)
1 parent b607226 commit 5af5d29

File tree

3 files changed

+67
-20
lines changed

3 files changed

+67
-20
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
"remark-prism": "^1.3.6",
9999
"rss": "^1.2.2",
100100
"sass": "^1.69.5",
101-
"search-insights": "^2.2.3",
101+
"search-insights": "^2.17.2",
102102
"server-only": "^0.0.1",
103103
"sharp": "^0.33.4",
104104
"tailwindcss-scoped-preflight": "^3.0.4",
@@ -137,4 +137,4 @@
137137
"node": "20.11.0",
138138
"yarn": "1.22.21"
139139
}
140-
}
140+
}

src/components/search/index.tsx

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

33
import {Fragment, useCallback, useEffect, useRef, useState} from 'react';
4+
import {captureException} from '@sentry/nextjs';
45
import {
56
Hit,
67
Result,
@@ -9,7 +10,7 @@ import {
910
} from '@sentry-internal/global-search';
1011
import DOMPurify from 'dompurify';
1112
import Link from 'next/link';
12-
import {useRouter} from 'next/navigation';
13+
import {usePathname, useRouter} from 'next/navigation';
1314
import algoliaInsights from 'search-insights';
1415

1516
import {useOnClickOutside} from 'sentry-docs/clientUtils';
@@ -90,6 +91,7 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro
9091
const [showOffsiteResults, setShowOffsiteResults] = useState(false);
9192
const [loading, setLoading] = useState(true);
9293
const router = useRouter();
94+
const pathname = usePathname();
9395

9496
const handleClickOutside = useCallback((ev: MouseEvent) => {
9597
// don't close the search results if the user is clicking the expand button
@@ -196,21 +198,66 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro
196198
});
197199

198200
const trackSearchResultClick = useCallback((hit: Hit, position: number): void => {
199-
if (hit.id === undefined) {
200-
return;
201+
try {
202+
algoliaInsights('clickedObjectIDsAfterSearch', {
203+
eventName: 'documentation_search_result_click',
204+
userToken: randomUserToken,
205+
index: hit.index,
206+
objectIDs: [hit.id],
207+
// Positions in Algolia are 1 indexed
208+
queryID: hit.queryID ?? '',
209+
positions: [position + 1],
210+
});
211+
} catch (error) {
212+
captureException(error);
201213
}
214+
}, []);
202215

203-
algoliaInsights('clickedObjectIDsAfterSearch', {
204-
eventName: 'documentation_search_result_click',
205-
userToken: randomUserToken,
206-
index: hit.index,
207-
objectIDs: [hit.id],
208-
// Positions in Algolia are 1 indexed
209-
queryID: hit.queryID ?? '',
210-
positions: [position + 1],
211-
});
216+
const removeTags = useCallback((str: string) => {
217+
return str.replace(/<\/?[^>]+(>|$)/g, '');
212218
}, []);
213219

220+
const handleSearchResultClick = useCallback(
221+
(event: React.MouseEvent<HTMLAnchorElement>, hit: Hit, position: number): void => {
222+
if (hit.id === undefined) {
223+
return;
224+
}
225+
226+
trackSearchResultClick(hit, position);
227+
228+
// edge case when the clicked search result is the currently visited paged
229+
if (relativizeUrl(hit.url) === pathname) {
230+
// do not navigate to the search result page in this case
231+
event.preventDefault();
232+
233+
// sanitize the title to remove any html tags
234+
const title = hit?.title && removeTags(hit.title);
235+
236+
if (!title) {
237+
return;
238+
}
239+
240+
// check for heading with the same text as the title
241+
const headings =
242+
document
243+
.querySelector('main > div.prose')
244+
?.querySelectorAll('h1, h2, h3, h4, h5, h6') ?? [];
245+
const foundHeading = Array.from(headings).find(heading =>
246+
heading.textContent?.toLowerCase().includes(title.toLowerCase())
247+
);
248+
249+
// close the search results and scroll to the heading if it exists
250+
setInputFocus(false);
251+
if (foundHeading) {
252+
foundHeading.scrollIntoView({
253+
behavior: 'smooth',
254+
});
255+
}
256+
}
257+
},
258+
[pathname, removeTags, trackSearchResultClick]
259+
);
260+
214261
return (
215262
<div className={styles.search} ref={ref}>
216263
<div className={styles['search-bar']}>
@@ -271,15 +318,15 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro
271318
focused?.id === hit.id ? styles['sgs-hit-focused'] : ''
272319
}`}
273320
ref={
274-
// Scroll to eleemnt on focus
321+
// Scroll to element on focus
275322
hit.id === focused?.id
276323
? el => el?.scrollIntoView({block: 'nearest'})
277324
: undefined
278325
}
279326
>
280327
<Link
281328
href={relativizeUrl(hit.url)}
282-
onClick={() => trackSearchResultClick(hit, index)}
329+
onClick={e => handleSearchResultClick(e, hit, index)}
283330
>
284331
{hit.title && (
285332
<h6>

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11110,10 +11110,10 @@ scheduler@^0.23.0:
1111011110
dependencies:
1111111111
loose-envify "^1.1.0"
1111211112

11113-
search-insights@^2.2.3:
11114-
version "2.13.0"
11115-
resolved "https://registry.npmjs.org/search-insights/-/search-insights-2.13.0.tgz"
11116-
integrity sha512-Orrsjf9trHHxFRuo9/rzm0KIWmgzE8RMlZMzuhZOJ01Rnz3D0YBAe+V6473t6/H6c7irs6Lt48brULAiRWb3Vw==
11113+
search-insights@^2.17.2:
11114+
version "2.17.2"
11115+
resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.17.2.tgz#d13b2cabd44e15ade8f85f1c3b65c8c02138629a"
11116+
integrity sha512-zFNpOpUO+tY2D85KrxJ+aqwnIfdEGi06UH2+xEb+Bp9Mwznmauqc9djbnBibJO5mpfUPPa8st6Sx65+vbeO45g==
1111711117

1111811118
section-matter@^1.0.0:
1111911119
version "1.0.0"

0 commit comments

Comments
 (0)