@@ -336,6 +336,111 @@ describe('CommandPalette', () => {
336
336
} ) ;
337
337
} ) ;
338
338
339
+ it ( 'auto-focuses first item when search input is focused and has content' , async ( ) => {
340
+ const user = userEvent . setup ( ) ;
341
+
342
+ render (
343
+ < CommandPalette >
344
+ { items . map ( ( item ) => (
345
+ < CommandPalette . Item key = { item . id } id = { item . id } >
346
+ { item . textValue }
347
+ </ CommandPalette . Item >
348
+ ) ) }
349
+ </ CommandPalette > ,
350
+ ) ;
351
+
352
+ const searchInput = screen . getByPlaceholderText ( 'Search commands...' ) ;
353
+
354
+ // Initially no item should be focused when just clicking
355
+ await user . click ( searchInput ) ;
356
+ expect ( searchInput . getAttribute ( 'aria-activedescendant' ) ) . toBeFalsy ( ) ;
357
+
358
+ // Type something to trigger auto-focus
359
+ await user . type ( searchInput , 'C' ) ;
360
+
361
+ // Check that the first matching item is virtually focused
362
+ await waitFor ( ( ) => {
363
+ const activeDescendant = searchInput . getAttribute (
364
+ 'aria-activedescendant' ,
365
+ ) ;
366
+ expect ( activeDescendant ) . toMatch ( / C o m m a n d P a l e t t e - m e n u - o p t i o n - 1 / ) ;
367
+ } ) ;
368
+ } ) ;
369
+
370
+ it ( 'focuses first item in filtered results when search changes' , async ( ) => {
371
+ const user = userEvent . setup ( ) ;
372
+
373
+ render (
374
+ < CommandPalette >
375
+ { items . map ( ( item ) => (
376
+ < CommandPalette . Item key = { item . id } id = { item . id } >
377
+ { item . textValue }
378
+ </ CommandPalette . Item >
379
+ ) ) }
380
+ </ CommandPalette > ,
381
+ ) ;
382
+
383
+ const searchInput = screen . getByPlaceholderText ( 'Search commands...' ) ;
384
+
385
+ // Focus and type to filter items - "folder" should match "Open folder" (id: '2')
386
+ await user . click ( searchInput ) ;
387
+ await user . type ( searchInput , 'folder' ) ;
388
+
389
+ // First verify that the item is actually filtered and visible
390
+ expect ( screen . getByText ( 'Open folder' ) ) . toBeInTheDocument ( ) ;
391
+ expect ( screen . queryByText ( 'Create file' ) ) . not . toBeInTheDocument ( ) ;
392
+
393
+ // Check that the correct item is virtually focused
394
+ await waitFor ( ( ) => {
395
+ const activeDescendant = searchInput . getAttribute (
396
+ 'aria-activedescendant' ,
397
+ ) ;
398
+ expect ( activeDescendant ) . toBeTruthy ( ) ;
399
+ // Since "Open folder" has id="2", it should be focused when filtered
400
+ expect ( activeDescendant ) . toContain ( 'menu-option-2' ) ;
401
+ } ) ;
402
+ } ) ;
403
+
404
+ it ( 'clears focus when no items match search' , async ( ) => {
405
+ const user = userEvent . setup ( ) ;
406
+
407
+ render (
408
+ < CommandPalette >
409
+ { items . map ( ( item ) => (
410
+ < CommandPalette . Item key = { item . id } id = { item . id } >
411
+ { item . textValue }
412
+ </ CommandPalette . Item >
413
+ ) ) }
414
+ </ CommandPalette > ,
415
+ ) ;
416
+
417
+ const searchInput = screen . getByPlaceholderText ( 'Search commands...' ) ;
418
+
419
+ // First type something that matches to establish focus
420
+ await user . click ( searchInput ) ;
421
+ await user . type ( searchInput , 'C' ) ;
422
+
423
+ // Verify an item is focused first
424
+ await waitFor ( ( ) => {
425
+ const activeDescendant = searchInput . getAttribute (
426
+ 'aria-activedescendant' ,
427
+ ) ;
428
+ expect ( activeDescendant ) . toMatch ( / C o m m a n d P a l e t t e - m e n u - o p t i o n - 1 / ) ;
429
+ } ) ;
430
+
431
+ // Clear and type something that won't match
432
+ await user . clear ( searchInput ) ;
433
+ await user . type ( searchInput , 'nonexistent' ) ;
434
+
435
+ // Check that no item is focused
436
+ await waitFor ( ( ) => {
437
+ const activeDescendant = searchInput . getAttribute (
438
+ 'aria-activedescendant' ,
439
+ ) ;
440
+ expect ( activeDescendant ) . toBeFalsy ( ) ;
441
+ } ) ;
442
+ } ) ;
443
+
339
444
it ( 'supports custom empty label' , async ( ) => {
340
445
const user = userEvent . setup ( ) ;
341
446
0 commit comments