@@ -5,22 +5,22 @@ import {
55 ChevronLeftIcon ,
66} from '@heroicons/react/24/outline' ;
77import type { Results , Nullable } from '@orama/orama' ;
8- import classNames from 'classnames' ;
98import { useState , useRef , useEffect } from 'react' ;
109import type { FC } from 'react' ;
1110
1211import styles from '@/components/Common/Search/States/index.module.css' ;
1312import { WithAllResults } from '@/components/Common/Search/States/WithAllResults' ;
14- import { WithEmptyState } from '@/components/Common/Search/States/WithEmptyState' ;
1513import { WithError } from '@/components/Common/Search/States/WithError' ;
1614import { WithNoResults } from '@/components/Common/Search/States/WithNoResults' ;
1715import { WithPoweredBy } from '@/components/Common/Search/States/WithPoweredBy' ;
1816import { 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' ;
2019import { useRouter } from '@/navigation.mjs' ;
2120import { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs' ;
2221import { search as oramaSearch , getInitialFacets } from '@/next.orama.mjs' ;
2322import type { SearchDoc } from '@/types' ;
23+ import { searchHitToLinkPath } from '@/util/searchUtils' ;
2424
2525type Facets = { [ key : string ] : number } ;
2626
@@ -31,6 +31,7 @@ type SearchBoxProps = { onClose: () => void };
3131export const WithSearchBox : FC < SearchBoxProps > = ( { onClose } ) => {
3232 const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
3333 const [ searchResults , setSearchResults ] = useState < SearchResults > ( null ) ;
34+ const [ selectedResult , setSelectedResult ] = useState < number > ( ) ;
3435 const [ selectedFacet , setSelectedFacet ] = useState < number > ( 0 ) ;
3536 const [ searchError , setSearchError ] = useState < Nullable < Error > > ( null ) ;
3637
@@ -56,10 +57,19 @@ export const WithSearchBox: FC<SearchBoxProps> = ({ onClose }) => {
5657 . catch ( setSearchError ) ;
5758 } ;
5859
59- useClickOutside ( searchBoxRef , ( ) => {
60+ const reset = ( ) => {
61+ setSearchTerm ( '' ) ;
62+ setSearchResults ( null ) ;
63+ setSelectedResult ( undefined ) ;
64+ setSelectedFacet ( 0 ) ;
65+ } ;
66+
67+ const handleClose = ( ) => {
6068 reset ( ) ;
6169 onClose ( ) ;
62- } ) ;
70+ } ;
71+
72+ useClickOutside ( searchBoxRef , handleClose ) ;
6373
6474 useEffect ( ( ) => {
6575 searchInputRef . current ?. focus ( ) ;
@@ -78,19 +88,54 @@ export const WithSearchBox: FC<SearchBoxProps> = ({ onClose }) => {
7888 [ searchTerm , selectedFacet ]
7989 ) ;
8090
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 ) ) ;
85129 } ;
86130
87131 const onSubmit = ( e : React . FormEvent < HTMLFormElement > ) => {
88132 e . preventDefault ( ) ;
133+
134+ handleClose ( ) ;
89135 router . push ( `/search?q=${ searchTerm } §ion=${ selectedFacetName } ` ) ;
90- onClose ( ) ;
91136 } ;
92137
93- const changeFacet = ( idx : number ) => setSelectedFacet ( idx ) ;
138+ const changeFacet = ( idx : string ) => setSelectedFacet ( Number ( idx ) ) ;
94139
95140 const filterBySection = ( ) => {
96141 if ( selectedFacet === 0 ) {
@@ -131,6 +176,16 @@ export const WithSearchBox: FC<SearchBoxProps> = ({ onClose }) => {
131176 < form onSubmit = { onSubmit } >
132177 < input
133178 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"
134189 type = "search"
135190 className = { styles . searchBoxInput }
136191 onChange = { event => setSearchTerm ( event . target . value ) }
@@ -140,36 +195,38 @@ export const WithSearchBox: FC<SearchBoxProps> = ({ onClose }) => {
140195 </ div >
141196
142197 < 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+ />
157210 </ div >
158211
159- < div className = { styles . fulltextResultsContainer } >
212+ < div
213+ id = "fulltext-results-container"
214+ className = { styles . fulltextResultsContainer }
215+ role = "listbox"
216+ >
160217 { searchError && < WithError /> }
161218
162- { ! searchError && ! searchTerm && < WithEmptyState /> }
163-
164- { ! searchError && searchTerm && (
219+ { ! searchError && (
165220 < >
166221 { searchResults &&
167222 searchResults . count > 0 &&
168- searchResults . hits . map ( hit => (
223+ searchResults . hits . map ( ( hit , idx ) => (
169224 < WithSearchResult
170225 key = { hit . id }
171226 hit = { hit }
172227 searchTerm = { searchTerm }
228+ selected = { selectedResult === idx }
229+ idx = { idx }
173230 />
174231 ) ) }
175232
0 commit comments