Skip to content

Commit 78bb6fa

Browse files
committed
refactor
1 parent 536fe71 commit 78bb6fa

File tree

4 files changed

+134
-107
lines changed

4 files changed

+134
-107
lines changed

src/components/search/index.tsx

Lines changed: 12 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ import {
99
SentryGlobalSearch,
1010
standardSDKSlug,
1111
} from '@sentry-internal/global-search';
12-
import DOMPurify from 'dompurify';
13-
import Link from 'next/link';
14-
import {usePathname, useRouter} from 'next/navigation';
12+
import {usePathname} from 'next/navigation';
1513
import algoliaInsights from 'search-insights';
1614

1715
import {useOnClickOutside} from 'sentry-docs/clientUtils';
18-
import {useListKeyboardNavigate} from 'sentry-docs/hooks/useListKeyboardNavigate';
1916
import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs';
2017

2118
import styles from './search.module.scss';
2219

2320
import {Logo} from '../logo';
2421

22+
import {SearchResultItems} from './searchResultItems';
23+
import {relativizeUrl} from './util';
24+
2525
// Initialize Algolia Insights
2626
algoliaInsights('init', {
2727
appId: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
@@ -33,8 +33,6 @@ algoliaInsights('init', {
3333
// treat it as a random user.
3434
const randomUserToken = crypto.randomUUID();
3535

36-
const MAX_HITS = 10;
37-
3836
// this type is not exported from the global-search package
3937
type SentryGlobalSearchConfig = ConstructorParameters<typeof SentryGlobalSearch>[0];
4038

@@ -59,12 +57,6 @@ const userDocsSites: SentryGlobalSearchConfig = [
5957
const config = isDeveloperDocs ? developerDocsSites : userDocsSites;
6058
const search = new SentryGlobalSearch(config);
6159

62-
function relativizeUrl(url: string) {
63-
return isDeveloperDocs
64-
? url
65-
: url.replace(/^(https?:\/\/docs\.sentry\.io)(?=\/|$)/, '');
66-
}
67-
6860
type Props = {
6961
autoFocus?: boolean;
7062
path?: string;
@@ -79,7 +71,7 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro
7971
const [inputFocus, setInputFocus] = useState(false);
8072
const [showOffsiteResults, setShowOffsiteResults] = useState(false);
8173
const [loading, setLoading] = useState(true);
82-
const router = useRouter();
74+
8375
const pathname = usePathname();
8476

8577
const handleClickOutside = useCallback((ev: MouseEvent) => {
@@ -176,17 +168,6 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro
176168

177169
const totalHits = results.reduce((a, x) => a + x.hits.length, 0);
178170

179-
const flatHits = results.reduce<Hit[]>(
180-
(items, item) => [...items, ...item.hits.slice(0, MAX_HITS)],
181-
[]
182-
);
183-
184-
const {focused} = useListKeyboardNavigate({
185-
list: flatHits,
186-
onSelect: hit => router.push(relativizeUrl(hit.url)),
187-
disableEventListeners: !inputFocus,
188-
});
189-
190171
const trackSearchResultClick = useCallback((hit: Hit, position: number): void => {
191172
try {
192173
algoliaInsights('clickedObjectIDsAfterSearch', {
@@ -306,77 +287,13 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro
306287
{loading && <Logo loading />}
307288

308289
{!loading && totalHits > 0 && (
309-
<div className={styles['sgs-search-results-scroll-container']}>
310-
{results
311-
.filter(x => x.hits.length > 0)
312-
.map((result, i) => (
313-
<Fragment key={result.site}>
314-
{showOffsiteResults && (
315-
<h4 className={styles['sgs-site-result-heading']}>
316-
From {result.name}
317-
</h4>
318-
)}
319-
<ul
320-
className={`${styles['sgs-hit-list']} ${i === 0 ? '' : styles['sgs-offsite']}`}
321-
>
322-
{result.hits.slice(0, MAX_HITS).map((hit, index) => (
323-
<li
324-
key={hit.id}
325-
className={`${styles['sgs-hit-item']} ${
326-
focused?.id === hit.id ? styles['sgs-hit-focused'] : ''
327-
}`}
328-
ref={
329-
// Scroll to element on focus
330-
hit.id === focused?.id
331-
? el => el?.scrollIntoView({block: 'nearest'})
332-
: undefined
333-
}
334-
>
335-
<Link
336-
href={relativizeUrl(hit.url)}
337-
onClick={e => handleSearchResultClick(e, hit, index)}
338-
>
339-
{hit.title && (
340-
<h6>
341-
<span
342-
dangerouslySetInnerHTML={{
343-
__html: DOMPurify.sanitize(hit.title, {
344-
ALLOWED_TAGS: ['mark'],
345-
}),
346-
}}
347-
/>
348-
</h6>
349-
)}
350-
{hit.text && (
351-
<span
352-
dangerouslySetInnerHTML={{
353-
__html: DOMPurify.sanitize(hit.text, {
354-
ALLOWED_TAGS: ['mark'],
355-
}),
356-
}}
357-
/>
358-
)}
359-
{hit.context && (
360-
<div className={styles['sgs-hit-context']}>
361-
{hit.context.context1 && (
362-
<div className={styles['sgs-hit-context-left']}>
363-
{hit.context.context1}
364-
</div>
365-
)}
366-
{hit.context.context2 && (
367-
<div className={styles['sgs-hit-context-right']}>
368-
{hit.context.context2}
369-
</div>
370-
)}
371-
</div>
372-
)}
373-
</Link>
374-
</li>
375-
))}
376-
</ul>
377-
</Fragment>
378-
))}
379-
</div>
290+
<SearchResultItems
291+
results={results}
292+
onSearchResultClick={({event, hit, position}) =>
293+
handleSearchResultClick(event, hit, position)
294+
}
295+
showOffsiteResults={showOffsiteResults}
296+
/>
380297
)}
381298

382299
{!loading && totalHits === 0 && (
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {Fragment} from 'react';
2+
import {Hit, Result} from '@sentry-internal/global-search';
3+
import DOMPurify from 'dompurify';
4+
import Link from 'next/link';
5+
import {useRouter} from 'next/navigation';
6+
7+
import {useListKeyboardNavigate} from 'sentry-docs/hooks/useListKeyboardNavigate';
8+
9+
import styles from './search.module.scss';
10+
11+
import {relativizeUrl} from './util';
12+
13+
const MAX_HITS = 10;
14+
15+
interface SearchResultClickHandler {
16+
event: React.MouseEvent<HTMLAnchorElement>;
17+
hit: Hit;
18+
position: number;
19+
}
20+
21+
export function SearchResultItems({
22+
results,
23+
showOffsiteResults,
24+
onSearchResultClick,
25+
}: {
26+
onSearchResultClick: (params: SearchResultClickHandler) => void;
27+
results: Result[];
28+
showOffsiteResults: boolean;
29+
}) {
30+
const router = useRouter();
31+
const flatHits = results.reduce<Hit[]>(
32+
(items, item) => [...items, ...item.hits.slice(0, MAX_HITS)],
33+
[]
34+
);
35+
const {focused} = useListKeyboardNavigate({
36+
list: flatHits,
37+
onSelect: hit => router.push(relativizeUrl(hit.url)),
38+
});
39+
40+
return (
41+
<div className={styles['sgs-search-results-scroll-container']}>
42+
{results
43+
.filter(x => x.hits.length > 0)
44+
.map((result, i) => (
45+
<Fragment key={result.site}>
46+
{showOffsiteResults && (
47+
<h4 className={styles['sgs-site-result-heading']}>From {result.name}</h4>
48+
)}
49+
<ul
50+
className={`${styles['sgs-hit-list']} ${i === 0 ? '' : styles['sgs-offsite']}`}
51+
>
52+
{result.hits.slice(0, MAX_HITS).map((hit, index) => (
53+
<li
54+
key={hit.id}
55+
className={`${styles['sgs-hit-item']} ${
56+
focused?.id === hit.id ? styles['sgs-hit-focused'] : ''
57+
}`}
58+
ref={
59+
// Scroll to element on focus
60+
hit.id === focused?.id
61+
? el => el?.scrollIntoView({block: 'nearest'})
62+
: undefined
63+
}
64+
>
65+
<Link
66+
href={relativizeUrl(hit.url)}
67+
onClick={event => onSearchResultClick({event, hit, position: index})}
68+
>
69+
{hit.title && (
70+
<h6>
71+
<span
72+
dangerouslySetInnerHTML={{
73+
__html: DOMPurify.sanitize(hit.title, {
74+
ALLOWED_TAGS: ['mark'],
75+
}),
76+
}}
77+
/>
78+
</h6>
79+
)}
80+
{hit.text && (
81+
<span
82+
dangerouslySetInnerHTML={{
83+
__html: DOMPurify.sanitize(hit.text, {
84+
ALLOWED_TAGS: ['mark'],
85+
}),
86+
}}
87+
/>
88+
)}
89+
{hit.context && (
90+
<div className={styles['sgs-hit-context']}>
91+
{hit.context.context1 && (
92+
<div className={styles['sgs-hit-context-left']}>
93+
{hit.context.context1}
94+
</div>
95+
)}
96+
{hit.context.context2 && (
97+
<div className={styles['sgs-hit-context-right']}>
98+
{hit.context.context2}
99+
</div>
100+
)}
101+
</div>
102+
)}
103+
</Link>
104+
</li>
105+
))}
106+
</ul>
107+
</Fragment>
108+
))}
109+
</div>
110+
);
111+
}

src/components/search/util.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {isDeveloperDocs} from 'sentry-docs/isDeveloperDocs';
2+
3+
export function relativizeUrl(url: string) {
4+
return isDeveloperDocs
5+
? url
6+
: url.replace(/^(https?:\/\/docs\.sentry\.io)(?=\/|$)/, '');
7+
}

src/hooks/useListKeyboardNavigate.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import {useCallback, useEffect, useMemo, useState} from 'react';
22

33
type Props<T> = {
4-
/**
5-
* Whether to disable the keyboard event listeners conditionally
6-
*/
7-
disableEventListeners: boolean;
84
/**
95
* The list of values to navigate through
106
*/
@@ -19,7 +15,7 @@ type Props<T> = {
1915
/**
2016
* Navigate a list of items using the up/down arrow and ^j/^k keys
2117
*/
22-
function useListKeyboardNavigate<T>({list, onSelect, disableEventListeners}: Props<T>) {
18+
function useListKeyboardNavigate<T>({list, onSelect}: Props<T>) {
2319
const [focused, setFocus] = useState<T | null>(null);
2420

2521
const setFocusIndex = useCallback(
@@ -90,13 +86,9 @@ function useListKeyboardNavigate<T>({list, onSelect, disableEventListeners}: Pro
9086
);
9187

9288
useEffect(() => {
93-
if (!disableEventListeners) {
94-
document.addEventListener('keydown', handleNavigate);
95-
return () => document.removeEventListener('keydown', handleNavigate);
96-
}
97-
98-
return undefined;
99-
}, [disableEventListeners, handleNavigate]);
89+
document.addEventListener('keydown', handleNavigate);
90+
return () => document.removeEventListener('keydown', handleNavigate);
91+
}, [handleNavigate]);
10092

10193
return {focused, setFocus};
10294
}

0 commit comments

Comments
 (0)