Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit e12cc98

Browse files
authored
feat: add keyboard navigation to search results 🎉 (#3090)
* Feat: add keyboard navigation to search results
1 parent b425e39 commit e12cc98

1 file changed

Lines changed: 40 additions & 1 deletion

File tree

  • src/components/CommonComponents/SearchBar

src/components/CommonComponents/SearchBar/index.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ const SearchBar = (): JSX.Element => {
4343

4444
const isEmpty = !results || results.length === 0;
4545

46+
const listRef = useRef<HTMLUListElement | null>(null);
47+
48+
const activeIndexRef = useRef<number>(-1);
49+
4650
const changeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
4751
e.preventDefault();
4852

@@ -56,6 +60,7 @@ const SearchBar = (): JSX.Element => {
5660
const collapseContainer = () => {
5761
setExpanded(false);
5862
setQuery('');
63+
activeIndexRef.current = -1;
5964
};
6065

6166
useEffect(() => {
@@ -94,6 +99,14 @@ const SearchBar = (): JSX.Element => {
9499
const toggleContainer = () =>
95100
isExpanded ? collapseContainer() : expandContainer();
96101

102+
const focusActiveSearchItem = () => {
103+
if (listRef.current) {
104+
const el = listRef.current.children[activeIndexRef.current];
105+
el?.querySelector('a')?.focus();
106+
el?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
107+
}
108+
};
109+
97110
useKeyPress({
98111
targetKey: 'ctrl+k',
99112
callback: toggleContainer,
@@ -115,6 +128,31 @@ const SearchBar = (): JSX.Element => {
115128
},
116129
});
117130

131+
const handleSearchResultFocus = (e: React.KeyboardEvent) => {
132+
if (!isExpanded || isEmpty || !listRef.current) return;
133+
134+
if (e.key === 'ArrowDown') {
135+
e.preventDefault();
136+
activeIndexRef.current =
137+
activeIndexRef.current + 1 > listRef.current.children.length - 1
138+
? 0
139+
: activeIndexRef.current + 1;
140+
141+
focusActiveSearchItem();
142+
}
143+
144+
if (e.key === 'ArrowUp') {
145+
e.preventDefault();
146+
147+
activeIndexRef.current =
148+
activeIndexRef.current - 1 < 0
149+
? listRef.current.children.length - 1
150+
: activeIndexRef.current - 1;
151+
152+
focusActiveSearchItem();
153+
}
154+
};
155+
118156
return (
119157
<motion.div
120158
className={containerClassNames}
@@ -124,6 +162,7 @@ const SearchBar = (): JSX.Element => {
124162
transition={containerTransition}
125163
ref={parentRef}
126164
onBlur={onBlurHandler}
165+
onKeyDown={handleSearchResultFocus}
127166
>
128167
<div
129168
className={styles.searchInputContainer}
@@ -181,7 +220,7 @@ const SearchBar = (): JSX.Element => {
181220
</div>
182221
)}
183222
{!isEmpty && (
184-
<ul>
223+
<ul ref={listRef}>
185224
{results.map((result: SearchResult) => {
186225
const sectionPath =
187226
result.category === 'api'

0 commit comments

Comments
 (0)