@@ -5,22 +5,22 @@ import {
5
5
ChevronLeftIcon ,
6
6
} from '@heroicons/react/24/outline' ;
7
7
import type { Results , Nullable } from '@orama/orama' ;
8
- import classNames from 'classnames' ;
9
8
import { useState , useRef , useEffect } from 'react' ;
10
9
import type { FC } from 'react' ;
11
10
12
11
import styles from '@/components/Common/Search/States/index.module.css' ;
13
12
import { WithAllResults } from '@/components/Common/Search/States/WithAllResults' ;
14
- import { WithEmptyState } from '@/components/Common/Search/States/WithEmptyState' ;
15
13
import { WithError } from '@/components/Common/Search/States/WithError' ;
16
14
import { WithNoResults } from '@/components/Common/Search/States/WithNoResults' ;
17
15
import { WithPoweredBy } from '@/components/Common/Search/States/WithPoweredBy' ;
18
16
import { WithSearchResult } from '@/components/Common/Search/States/WithSearchResult' ;
19
- import { useClickOutside } from '@/hooks/react-client' ;
17
+ import Tabs from '@/components/Common/Tabs' ;
18
+ import { useClickOutside , useKeyboardCommands } from '@/hooks/react-client' ;
20
19
import { useRouter } from '@/navigation.mjs' ;
21
20
import { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs' ;
22
21
import { search as oramaSearch , getInitialFacets } from '@/next.orama.mjs' ;
23
22
import type { SearchDoc } from '@/types' ;
23
+ import { searchHitToLinkPath } from '@/util/searchUtils' ;
24
24
25
25
type Facets = { [ key : string ] : number } ;
26
26
@@ -31,6 +31,7 @@ type SearchBoxProps = { onClose: () => void };
31
31
export const WithSearchBox : FC < SearchBoxProps > = ( { onClose } ) => {
32
32
const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
33
33
const [ searchResults , setSearchResults ] = useState < SearchResults > ( null ) ;
34
+ const [ selectedResult , setSelectedResult ] = useState < number > ( ) ;
34
35
const [ selectedFacet , setSelectedFacet ] = useState < number > ( 0 ) ;
35
36
const [ searchError , setSearchError ] = useState < Nullable < Error > > ( null ) ;
36
37
@@ -56,10 +57,19 @@ export const WithSearchBox: FC<SearchBoxProps> = ({ onClose }) => {
56
57
. catch ( setSearchError ) ;
57
58
} ;
58
59
59
- useClickOutside ( searchBoxRef , ( ) => {
60
+ const reset = ( ) => {
61
+ setSearchTerm ( '' ) ;
62
+ setSearchResults ( null ) ;
63
+ setSelectedResult ( undefined ) ;
64
+ setSelectedFacet ( 0 ) ;
65
+ } ;
66
+
67
+ const handleClose = ( ) => {
60
68
reset ( ) ;
61
69
onClose ( ) ;
62
- } ) ;
70
+ } ;
71
+
72
+ useClickOutside ( searchBoxRef , handleClose ) ;
63
73
64
74
useEffect ( ( ) => {
65
75
searchInputRef . current ?. focus ( ) ;
@@ -78,19 +88,54 @@ export const WithSearchBox: FC<SearchBoxProps> = ({ onClose }) => {
78
88
[ searchTerm , selectedFacet ]
79
89
) ;
80
90
81
- const reset = ( ) => {
82
- setSearchTerm ( '' ) ;
83
- setSearchResults ( null ) ;
84
- setSelectedFacet ( 0 ) ;
91
+ useKeyboardCommands ( cmd => {
92
+ if ( searchError || ! searchResults || searchResults . count <= 0 ) {
93
+ return ;
94
+ }
95
+
96
+ switch ( true ) {
97
+ case cmd === 'down' && selectedResult === undefined :
98
+ setSelectedResult ( 0 ) ;
99
+ break ;
100
+ case cmd === 'down' &&
101
+ selectedResult != undefined &&
102
+ selectedResult < searchResults . count &&
103
+ selectedResult < DEFAULT_ORAMA_QUERY_PARAMS . limit - 1 :
104
+ setSelectedResult ( selectedResult + 1 ) ;
105
+ break ;
106
+ case cmd === 'up' && selectedResult != undefined && selectedResult != 0 :
107
+ setSelectedResult ( selectedResult - 1 ) ;
108
+ break ;
109
+ case cmd === 'enter' :
110
+ handleEnter ( ) ;
111
+ break ;
112
+ default :
113
+ }
114
+ } ) ;
115
+
116
+ const handleEnter = ( ) => {
117
+ if ( ! searchResults || ! selectedResult ) {
118
+ return ;
119
+ }
120
+
121
+ const selectedHit = searchResults . hits [ selectedResult ] ;
122
+
123
+ if ( ! selectedHit ) {
124
+ return ;
125
+ }
126
+
127
+ handleClose ( ) ;
128
+ router . push ( searchHitToLinkPath ( selectedHit ) ) ;
85
129
} ;
86
130
87
131
const onSubmit = ( e : React . FormEvent < HTMLFormElement > ) => {
88
132
e . preventDefault ( ) ;
133
+
134
+ handleClose ( ) ;
89
135
router . push ( `/search?q=${ searchTerm } §ion=${ selectedFacetName } ` ) ;
90
- onClose ( ) ;
91
136
} ;
92
137
93
- const changeFacet = ( idx : number ) => setSelectedFacet ( idx ) ;
138
+ const changeFacet = ( idx : string ) => setSelectedFacet ( Number ( idx ) ) ;
94
139
95
140
const filterBySection = ( ) => {
96
141
if ( selectedFacet === 0 ) {
@@ -131,6 +176,16 @@ export const WithSearchBox: FC<SearchBoxProps> = ({ onClose }) => {
131
176
< form onSubmit = { onSubmit } >
132
177
< input
133
178
ref = { searchInputRef }
179
+ aria-activedescendant = {
180
+ selectedResult !== undefined
181
+ ? `search-hit-${ selectedResult } `
182
+ : undefined
183
+ }
184
+ aria-autocomplete = "list"
185
+ aria-controls = "fulltext-results-container"
186
+ aria-expanded = { Boolean ( ! searchError && searchResults ?. count ) }
187
+ autoComplete = "off"
188
+ role = "combobox"
134
189
type = "search"
135
190
className = { styles . searchBoxInput }
136
191
onChange = { event => setSearchTerm ( event . target . value ) }
@@ -140,36 +195,38 @@ export const WithSearchBox: FC<SearchBoxProps> = ({ onClose }) => {
140
195
</ div >
141
196
142
197
< div className = { styles . fulltextSearchSections } >
143
- { Object . keys ( facets ) . map ( ( facetName , idx ) => (
144
- < button
145
- key = { facetName }
146
- className = { classNames ( styles . fulltextSearchSection , {
147
- [ styles . fulltextSearchSectionSelected ] : selectedFacet === idx ,
148
- } ) }
149
- onClick = { ( ) => changeFacet ( idx ) }
150
- >
151
- { facetName }
152
- < span className = { styles . fulltextSearchSectionCount } >
153
- ({ facets [ facetName ] . toLocaleString ( 'en' ) } )
154
- </ span >
155
- </ button >
156
- ) ) }
198
+ < Tabs
199
+ activationMode = "manual"
200
+ defaultValue = "0"
201
+ autoFocus = { true }
202
+ tabs = { Object . keys ( facets ) . map ( ( facetName , idx ) => ( {
203
+ key : facetName ,
204
+ label : facetName ,
205
+ secondaryLabel : `(${ facets [ facetName ] . toLocaleString ( 'en' ) } )` ,
206
+ value : idx . toString ( ) ,
207
+ } ) ) }
208
+ onValueChange = { changeFacet }
209
+ />
157
210
</ div >
158
211
159
- < div className = { styles . fulltextResultsContainer } >
212
+ < div
213
+ id = "fulltext-results-container"
214
+ className = { styles . fulltextResultsContainer }
215
+ role = "listbox"
216
+ >
160
217
{ searchError && < WithError /> }
161
218
162
- { ! searchError && ! searchTerm && < WithEmptyState /> }
163
-
164
- { ! searchError && searchTerm && (
219
+ { ! searchError && (
165
220
< >
166
221
{ searchResults &&
167
222
searchResults . count > 0 &&
168
- searchResults . hits . map ( hit => (
223
+ searchResults . hits . map ( ( hit , idx ) => (
169
224
< WithSearchResult
170
225
key = { hit . id }
171
226
hit = { hit }
172
227
searchTerm = { searchTerm }
228
+ selected = { selectedResult === idx }
229
+ idx = { idx }
173
230
/>
174
231
) ) }
175
232
0 commit comments