@@ -23,6 +23,9 @@ export class SearchBar extends Interaction {
2323 "t-on-keydown" : this . onKeydown ,
2424 "t-on-search" : this . onSearch ,
2525 } ,
26+ ".o_search_result_item" : {
27+ "t-on-keydown" : this . onSearchResultKeydown ,
28+ } ,
2629 } ;
2730 autocompleteMinWidth = 300 ;
2831
@@ -197,17 +200,13 @@ export class SearchBar extends Interaction {
197200 case "Escape" :
198201 this . render ( ) ;
199202 break ;
200- case "ArrowUp" :
201203 case "ArrowDown" :
202204 ev . preventDefault ( ) ;
203205 if ( this . menuEl ) {
204- const focusableEls = [ this . inputEl , ...this . menuEl . children ] ;
205- const focusedEl = document . activeElement ;
206- const currentIndex = focusableEls . indexOf ( focusedEl ) || 0 ;
207- const delta = ev . key === "ArrowUp" ? focusableEls . length - 1 : 1 ;
208- const nextIndex = ( currentIndex + delta ) % focusableEls . length ;
209- const nextFocusedEl = focusableEls [ nextIndex ] ;
210- nextFocusedEl . focus ( ) ;
206+ const firstResult = this . menuEl . querySelector ( ".o_search_result_item" ) ;
207+ if ( firstResult ) {
208+ firstResult . focus ( ) ;
209+ }
211210 }
212211 break ;
213212 case "Enter" :
@@ -216,6 +215,130 @@ export class SearchBar extends Interaction {
216215 }
217216 }
218217
218+ /**
219+ * Handle keyboard navigation within search results
220+ * @param {KeyboardEvent } ev
221+ */
222+ onSearchResultKeydown ( ev ) {
223+ const allResults = [ ...this . menuEl . querySelectorAll ( ".o_search_result_item" ) ] ;
224+ const currentResult = ev . currentTarget ;
225+ const currentIndex = allResults . indexOf ( currentResult ) ;
226+
227+ switch ( ev . key ) {
228+ case "Escape" :
229+ this . render ( ) ;
230+ break ;
231+ case "ArrowUp" : {
232+ ev . preventDefault ( ) ;
233+ // Check if current element is in the first row
234+ const currentRect = currentResult . getBoundingClientRect ( ) ;
235+ const isFirstRow = allResults . every ( ( el , idx ) => {
236+ if ( idx === currentIndex ) {
237+ return true ;
238+ }
239+ const rect = el . getBoundingClientRect ( ) ;
240+ return rect . top >= currentRect . top - 5 ; // Allow small tolerance
241+ } ) ;
242+
243+ if ( isFirstRow ) {
244+ // Focus back to input when in first row
245+ this . inputEl . focus ( ) ;
246+ } else {
247+ this . navigateByDirection ( currentIndex , allResults , "up" ) ;
248+ }
249+ break ;
250+ }
251+ case "ArrowDown" :
252+ ev . preventDefault ( ) ;
253+ this . navigateByDirection ( currentIndex , allResults , "down" ) ;
254+ break ;
255+ case "ArrowLeft" :
256+ ev . preventDefault ( ) ;
257+ this . navigateByDirection ( currentIndex , allResults , "left" ) ;
258+ break ;
259+ case "ArrowRight" :
260+ ev . preventDefault ( ) ;
261+ this . navigateByDirection ( currentIndex , allResults , "right" ) ;
262+ break ;
263+ }
264+ }
265+
266+ /**
267+ * Navigate through search results based on their visual position
268+ * @param {number } currentIndex
269+ * @param {Array } allResults
270+ * @param {string } direction - "up", "down", "left", "right"
271+ */
272+ navigateByDirection ( currentIndex , allResults , direction ) {
273+ const currentRect = allResults [ currentIndex ] . getBoundingClientRect ( ) ;
274+ const currentCenterX = currentRect . left + currentRect . width / 2 ;
275+ const currentCenterY = currentRect . top + currentRect . height / 2 ;
276+
277+ let nextIndex = - 1 ;
278+ let bestDistance = Infinity ;
279+
280+ allResults . forEach ( ( el , index ) => {
281+ if ( index === currentIndex ) {
282+ return ;
283+ }
284+
285+ const rect = el . getBoundingClientRect ( ) ;
286+ const centerX = rect . left + rect . width / 2 ;
287+ const centerY = rect . top + rect . height / 2 ;
288+
289+ let isInDirection = false ;
290+ let distance = 0 ;
291+
292+ switch ( direction ) {
293+ case "down" :
294+ if ( centerY > currentCenterY ) {
295+ isInDirection = true ;
296+ distance = Math . sqrt (
297+ Math . pow ( centerY - currentCenterY , 2 ) +
298+ Math . pow ( Math . abs ( centerX - currentCenterX ) / 2 , 2 )
299+ ) ;
300+ }
301+ break ;
302+ case "up" :
303+ if ( centerY < currentCenterY ) {
304+ isInDirection = true ;
305+ distance = Math . sqrt (
306+ Math . pow ( currentCenterY - centerY , 2 ) +
307+ Math . pow ( Math . abs ( centerX - currentCenterX ) / 2 , 2 )
308+ ) ;
309+ }
310+ break ;
311+ case "right" :
312+ if ( centerX > currentCenterX ) {
313+ isInDirection = true ;
314+ distance = Math . sqrt (
315+ Math . pow ( centerX - currentCenterX , 2 ) +
316+ Math . pow ( Math . abs ( centerY - currentCenterY ) / 2 , 2 )
317+ ) ;
318+ }
319+ break ;
320+ case "left" :
321+ if ( centerX < currentCenterX ) {
322+ isInDirection = true ;
323+ distance = Math . sqrt (
324+ Math . pow ( currentCenterX - centerX , 2 ) +
325+ Math . pow ( Math . abs ( centerY - currentCenterY ) / 2 , 2 )
326+ ) ;
327+ }
328+ break ;
329+ }
330+
331+ if ( isInDirection && distance < bestDistance ) {
332+ bestDistance = distance ;
333+ nextIndex = index ;
334+ }
335+ } ) ;
336+
337+ if ( nextIndex >= 0 ) {
338+ allResults [ nextIndex ] . focus ( ) ;
339+ }
340+ }
341+
219342 /**
220343 * @param {MouseEvent } ev
221344 */
0 commit comments