@@ -244,6 +244,7 @@ describe('createPeopleSearch', () => {
244244 const input = form . querySelector ( 'input' ) as HTMLInputElement
245245 const label = form . querySelector ( 'label' ) as HTMLLabelElement
246246 const dropdown = form . querySelector ( '.people-search-dropdown' ) as HTMLDivElement
247+ const liveRegion = form . querySelector ( 'div[role="status"]' ) as HTMLDivElement
247248
248249 expect ( label ) . not . toBeNull ( )
249250 expect ( label . textContent ) . toBe ( 'Search for people' )
@@ -253,11 +254,14 @@ describe('createPeopleSearch', () => {
253254 expect ( input . getAttribute ( 'aria-labelledby' ) ) . toBe ( label . id )
254255 expect ( input . getAttribute ( 'aria-controls' ) ) . toBe ( dropdown . id )
255256 expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
257+ expect ( liveRegion ) . not . toBeNull ( )
258+ expect ( typeof liveRegion . textContent ) . toBe ( 'string' )
256259
257260 await openDropdown ( form )
258261
259262 const personRow = rowFor ( form , 'https://alice.example/profile/card#me' )
260263 expect ( dropdown . getAttribute ( 'role' ) ) . toBe ( 'listbox' )
264+ expect ( dropdown . getAttribute ( 'aria-busy' ) ) . toBe ( 'false' )
261265 expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
262266 expect ( personRow ?. getAttribute ( 'role' ) ) . toBe ( 'option' )
263267 expect ( personRow ?. id ) . toContain ( '-option-' )
@@ -294,16 +298,21 @@ describe('createPeopleSearch', () => {
294298 const input = form . querySelector ( 'input' ) as HTMLInputElement
295299 const dropdown = form . querySelector ( '.people-search-dropdown' ) as HTMLDivElement
296300 const aliceRow = rowFor ( form , 'https://alice.example/profile/card#me' ) as HTMLDivElement
301+ const bobRow = rowFor ( form , 'https://bob.example/profile/card#me' ) as HTMLDivElement
297302
298303 keyDown ( input , 'ArrowDown' )
299304 expect ( input . getAttribute ( 'aria-activedescendant' ) ) . toBe ( aliceRow . id )
300305 expect ( aliceRow . getAttribute ( 'aria-selected' ) ) . toBe ( 'true' )
301306
307+ keyDown ( input , 'ArrowUp' )
308+ expect ( input . getAttribute ( 'aria-activedescendant' ) ) . toBe ( bobRow . id )
309+ expect ( bobRow . getAttribute ( 'aria-selected' ) ) . toBe ( 'true' )
310+
302311 keyDown ( input , 'Enter' )
303312 expect ( onClickHandler ) . toHaveBeenCalledTimes ( 1 )
304313 expect ( onClickHandler ) . toHaveBeenCalledWith ( {
305- name : 'Alice Example ' ,
306- webId : 'https://alice .example/profile/card#me' ,
314+ name : 'Bob Stone ' ,
315+ webId : 'https://bob .example/profile/card#me' ,
307316 relationshipLabel : 'Contact'
308317 } )
309318 expect ( dropdown . style . display ) . toBe ( 'none' )
@@ -315,6 +324,50 @@ describe('createPeopleSearch', () => {
315324 expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
316325 } )
317326
327+ it ( 'supports Home/End navigation and closes on Tab' , async ( ) => {
328+ mockReadAddressBook . mockResolvedValue ( {
329+ contacts : [
330+ {
331+ uri : 'https://pod.example/contacts/1#this' ,
332+ name : 'Alice Example'
333+ } ,
334+ {
335+ uri : 'https://pod.example/contacts/2#this' ,
336+ name : 'Bob Stone'
337+ }
338+ ]
339+ } )
340+
341+ const kb = makeKb ( {
342+ contactWebIdsByCardUri : {
343+ 'https://pod.example/contacts/1#this' : 'https://alice.example/profile/card#me' ,
344+ 'https://pod.example/contacts/2#this' : 'https://bob.example/profile/card#me'
345+ }
346+ } )
347+ const me = new NamedNode ( 'https://user-11.example/profile/card#me' )
348+
349+ const form = createPeopleSearch ( document , kb as any , me )
350+ document . body . appendChild ( form )
351+
352+ await openDropdown ( form )
353+
354+ const input = form . querySelector ( 'input' ) as HTMLInputElement
355+ const dropdown = form . querySelector ( '.people-search-dropdown' ) as HTMLDivElement
356+ const aliceRow = rowFor ( form , 'https://alice.example/profile/card#me' ) as HTMLDivElement
357+ const bobRow = rowFor ( form , 'https://bob.example/profile/card#me' ) as HTMLDivElement
358+
359+ keyDown ( input , 'End' )
360+ expect ( input . getAttribute ( 'aria-activedescendant' ) ) . toBe ( bobRow . id )
361+
362+ keyDown ( input , 'Home' )
363+ expect ( input . getAttribute ( 'aria-activedescendant' ) ) . toBe ( aliceRow . id )
364+
365+ keyDown ( input , 'Tab' )
366+ expect ( dropdown . style . display ) . toBe ( 'none' )
367+ expect ( input . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
368+ expect ( input . getAttribute ( 'aria-activedescendant' ) ) . toBeNull ( )
369+ } )
370+
318371 it ( 'matches names by tokenized, case-insensitive words' , async ( ) => {
319372 mockReadAddressBook . mockResolvedValue ( {
320373 contacts : [
@@ -427,4 +480,19 @@ describe('createPeopleSearch', () => {
427480
428481 expect ( dropdown . textContent ) . toContain ( 'No contacts match that name.' )
429482 } )
483+
484+ it ( 'updates hidden live status text for no-match state' , async ( ) => {
485+ const kb = makeKb ( )
486+ const me = new NamedNode ( 'https://user-10.example/profile/card#me' )
487+
488+ const form = createPeopleSearch ( document , kb as any , me )
489+ document . body . appendChild ( form )
490+
491+ const liveRegion = form . querySelector ( 'div[role="status"]' ) as HTMLDivElement
492+
493+ await openDropdown ( form )
494+ await setSearchQuery ( form , 'no-person-will-match-this' )
495+
496+ expect ( liveRegion . textContent ) . toContain ( 'No contacts match that name.' )
497+ } )
430498} )
0 commit comments