Skip to content

Commit 0fe8b39

Browse files
chargomeJoshuaMoelans
authored andcommitted
fix(platform): Enable scrolling via keyboard buttons (#12460)
* enable scrolling via keyboard buttons * conditionally add event listeners in search * undo first try * refactor
1 parent c38df34 commit 0fe8b39

File tree

4 files changed

+133
-96
lines changed

4 files changed

+133
-96
lines changed

src/components/search/index.tsx

Lines changed: 12 additions & 94 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 {useKeyboardNavigate} from 'sentry-docs/hooks/useKeyboardNavigate';
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,16 +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} = useKeyboardNavigate({
185-
list: flatHits,
186-
onSelect: hit => router.push(relativizeUrl(hit.url)),
187-
});
188-
189171
const trackSearchResultClick = useCallback((hit: Hit, position: number): void => {
190172
try {
191173
algoliaInsights('clickedObjectIDsAfterSearch', {
@@ -305,77 +287,13 @@ export function Search({path, autoFocus, searchPlatforms = [], showChatBot}: Pro
305287
{loading && <Logo loading />}
306288

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

381299
{!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/useKeyboardNavigate.tsx renamed to src/hooks/useListKeyboardNavigate.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ type Props<T> = {
55
* The list of values to navigate through
66
*/
77
list: T[];
8+
89
/**
910
* Callback triggered when the item is selected
1011
*/
@@ -14,7 +15,7 @@ type Props<T> = {
1415
/**
1516
* Navigate a list of items using the up/down arrow and ^j/^k keys
1617
*/
17-
function useKeyboardNavigate<T>({list, onSelect}: Props<T>) {
18+
function useListKeyboardNavigate<T>({list, onSelect}: Props<T>) {
1819
const [focused, setFocus] = useState<T | null>(null);
1920

2021
const setFocusIndex = useCallback(
@@ -92,4 +93,4 @@ function useKeyboardNavigate<T>({list, onSelect}: Props<T>) {
9293
return {focused, setFocus};
9394
}
9495

95-
export {useKeyboardNavigate};
96+
export {useListKeyboardNavigate};

0 commit comments

Comments
 (0)