7979 .filter-badge-value {
8080 padding : 0 12px ;
8181 background-color : # f8faff ;
82+ flex-grow : 1 ; /* Allow this to grow to take available space */
8283 }
8384
8485 .filter-badge-remove {
8990 background-color : # f8faff ;
9091 border-left : 1px solid # cce0ff ;
9192 transition : background-color 0.2s ease;
93+ margin-left : auto; /* Push to the right end */
9294 }
9395
9496 .filter-badge-remove : hover {
231233
232234 .filter-badge {
233235 width : 100% ;
236+ justify-content : space-between; /* Ensure space distribution */
237+ }
238+
239+ .filter-badge-value {
240+ flex-grow : 1 ; /* Allow value to grow */
241+ }
242+
243+ .filter-badge-remove {
244+ margin-left : auto; /* Always push to end */
234245 }
235246 }
236247 </ style >
@@ -276,6 +287,7 @@ <h1>Keyboard accessible filter prototype</h1>
276287
277288// Current active filter being edited
278289let activeFilter = null ;
290+ let lastFocusedElementId = null ; // Track the last focused element
279291
280292// DOM references
281293const filterBadgesContainer = document . getElementById ( 'filterBadges' ) ;
@@ -316,15 +328,24 @@ <h1>Keyboard accessible filter prototype</h1>
316328
317329// Render filter badges
318330function renderFilterBadges ( ) {
331+ // Save focus state before re-rendering
332+ saveCurrentFocus ( ) ;
333+
319334 filterBadgesContainer . innerHTML = '' ;
320335
321336 filters . forEach ( filter => {
322337 const badge = document . createElement ( 'div' ) ;
323338 badge . className = 'filter-badge' ;
324339 badge . dataset . id = filter . id ;
325340
341+ // Create unique identifiers for each focusable element
342+ const columnId = `filter-column-${ filter . id } ` ;
343+ const operatorId = `filter-operator-${ filter . id } ` ;
344+ const valueId = `filter-value-${ filter . id } ` ;
345+ const removeId = `filter-remove-${ filter . id } ` ;
346+
326347 badge . innerHTML = `
327- <div class="filter-badge-part filter-badge-column" data-id="${ filter . id } ">
348+ <div id=" ${ columnId } " class="filter-badge-part filter-badge-column" data-id="${ filter . id } ">
328349 ${ filter . column }
329350 <select class="dropdown-select" data-id="${ filter . id } " tabindex="0" aria-label="Select column">
330351 <option value="">- remove filter -</option>
@@ -336,7 +357,7 @@ <h1>Keyboard accessible filter prototype</h1>
336357 <option value="keywords">keywords</option>
337358 </select>
338359 </div>
339- <div class="filter-badge-part filter-badge-operator" data-id="${ filter . id } ">
360+ <div id=" ${ operatorId } " class="filter-badge-part filter-badge-operator" data-id="${ filter . id } ">
340361 ${ operatorDisplay [ filter . operator ] || filter . operator }
341362 <select class="dropdown-select" data-id="${ filter . id } " tabindex="0" aria-label="Select operator">
342363 <option value="exact">=</option>
@@ -363,8 +384,8 @@ <h1>Keyboard accessible filter prototype</h1>
363384 <option value="notblank__1">is not blank</option>
364385 </select>
365386 </div>
366- <div class="filter-badge-part filter-badge-value" data-id="${ filter . id } " tabindex="0" role="button" aria-label="Edit value">${ filter . value } </div>
367- <div class="filter-badge-part filter-badge-remove" data-id="${ filter . id } " tabindex="0" role="button" aria-label="Remove filter">×</div>
387+ <div id=" ${ valueId } " class="filter-badge-part filter-badge-value" data-id="${ filter . id } " tabindex="0" role="button" aria-label="Edit value">${ filter . value } </div>
388+ <div id=" ${ removeId } " class="filter-badge-part filter-badge-remove" data-id="${ filter . id } " tabindex="0" role="button" aria-label="Remove filter">×</div>
368389 ` ;
369390
370391 filterBadgesContainer . appendChild ( badge ) ;
@@ -376,10 +397,45 @@ <h1>Keyboard accessible filter prototype</h1>
376397 if ( columnSelect ) columnSelect . value = filter . column ;
377398 if ( operatorSelect ) operatorSelect . value = filter . operator ;
378399 } ) ;
400+
401+ // Restore focus after re-rendering
402+ restoreFocus ( ) ;
403+ }
404+
405+ // Save current focused element
406+ function saveCurrentFocus ( ) {
407+ const activeElement = document . activeElement ;
408+ if ( activeElement && activeElement . closest ( '.filter-badge' ) ) {
409+ lastFocusedElementId = activeElement . id ||
410+ activeElement . closest ( '[id]' ) ?. id ||
411+ activeElement . closest ( '[data-id]' ) ?. dataset . id ;
412+ }
413+ }
414+
415+ // Restore focus to the previously focused element
416+ function restoreFocus ( ) {
417+ if ( lastFocusedElementId ) {
418+ const elementToFocus = document . getElementById ( lastFocusedElementId ) ;
419+ if ( elementToFocus ) {
420+ setTimeout ( ( ) => {
421+ elementToFocus . focus ( ) ;
422+ } , 0 ) ;
423+ }
424+ }
379425}
380426
381427// Setup event listeners
382428function setupEventListeners ( ) {
429+ // Track focus events
430+ document . addEventListener ( 'focusin' , ( e ) => {
431+ if ( e . target . closest ( '.filter-badge' ) || e . target . closest ( '.add-filter-badge' ) ) {
432+ const element = e . target . closest ( '[id]' ) ;
433+ if ( element ) {
434+ lastFocusedElementId = element . id ;
435+ }
436+ }
437+ } ) ;
438+
383439 // Add filter badge
384440 addFilterBadge . addEventListener ( 'click' , ( ) => {
385441 const newFilter = {
@@ -392,13 +448,18 @@ <h1>Keyboard accessible filter prototype</h1>
392448 filters . push ( newFilter ) ;
393449 renderFilterBadges ( ) ;
394450
395- // Auto-focus the column select
451+ // Auto-focus the column select of the new filter
396452 setTimeout ( ( ) => {
397- const columnSelect = document . querySelector ( `.filter-badge[data-id="${ newFilter . id } "] .filter-badge-column select` ) ;
398- if ( columnSelect ) {
399- columnSelect . focus ( ) ;
453+ const columnId = `filter-column-${ newFilter . id } ` ;
454+ const columnElement = document . getElementById ( columnId ) ;
455+ if ( columnElement ) {
456+ const columnSelect = columnElement . querySelector ( 'select' ) ;
457+ if ( columnSelect ) {
458+ columnSelect . focus ( ) ;
459+ lastFocusedElementId = columnId ;
460+ }
400461 }
401- } , 100 ) ;
462+ } , 50 ) ;
402463 } ) ;
403464
404465 // Add keyboard support for "add filter" button
@@ -423,7 +484,7 @@ <h1>Keyboard accessible filter prototype</h1>
423484 return ;
424485 }
425486
426- // Update the display text
487+ // Update the display text without full re-render
427488 e . target . parentElement . childNodes [ 0 ] . textContent = filter . column ;
428489 }
429490 }
@@ -437,7 +498,7 @@ <h1>Keyboard accessible filter prototype</h1>
437498 if ( filter ) {
438499 filter . operator = e . target . value ;
439500
440- // Update the display text
501+ // Update the display text without full re-render
441502 e . target . parentElement . childNodes [ 0 ] . textContent = operatorDisplay [ filter . operator ] || filter . operator ;
442503 }
443504 }
@@ -454,11 +515,41 @@ <h1>Keyboard accessible filter prototype</h1>
454515
455516 // Filter badge value keyboard
456517 document . addEventListener ( 'keydown' , ( e ) => {
457- if ( ( e . key === 'Enter' || e . key === ' ' ) && e . target . classList . contains ( 'filter-badge-value' ) ) {
458- e . preventDefault ( ) ;
459- const id = parseInt ( e . target . dataset . id ) ;
460- activeFilter = filters . find ( f => f . id === id ) ;
461- showValuePopover ( e . target ) ;
518+ if ( e . target . classList . contains ( 'filter-badge-value' ) ) {
519+ // Open popover on Enter/Space
520+ if ( e . key === 'Enter' || e . key === ' ' ) {
521+ e . preventDefault ( ) ;
522+ const id = parseInt ( e . target . dataset . id ) ;
523+ activeFilter = filters . find ( f => f . id === id ) ;
524+ showValuePopover ( e . target ) ;
525+ }
526+ // Open popover on any typing (except navigation keys)
527+ else if (
528+ e . key . length === 1 || // Single character keys
529+ e . key === 'Backspace' ||
530+ e . key === 'Delete'
531+ ) {
532+ e . preventDefault ( ) ;
533+ const id = parseInt ( e . target . dataset . id ) ;
534+ activeFilter = filters . find ( f => f . id === id ) ;
535+ showValuePopover ( e . target ) ;
536+
537+ // After popover is open, forward the typed character to the input
538+ setTimeout ( ( ) => {
539+ const valueInput = document . getElementById ( 'valueInput' ) ;
540+ if ( valueInput ) {
541+ if ( e . key . length === 1 ) {
542+ // For printable characters, set the input value to that character
543+ valueInput . value = e . key ;
544+ // Position cursor at the end
545+ valueInput . selectionStart = valueInput . selectionEnd = valueInput . value . length ;
546+ } else if ( e . key === 'Backspace' || e . key === 'Delete' ) {
547+ // For delete keys, start with empty value
548+ valueInput . value = '' ;
549+ }
550+ }
551+ } , 10 ) ;
552+ }
462553 }
463554 } ) ;
464555
@@ -513,6 +604,11 @@ <h1>Keyboard accessible filter prototype</h1>
513604
514605// Show value popover
515606function showValuePopover ( targetElement ) {
607+ // If popover is already active, don't reinitialize
608+ if ( valuePopover . classList . contains ( 'active' ) ) {
609+ return ;
610+ }
611+
516612 const rect = targetElement . getBoundingClientRect ( ) ;
517613
518614 // Calculate available space on the right
@@ -548,9 +644,11 @@ <h1>Keyboard accessible filter prototype</h1>
548644
549645 // Return focus to the value element that opened it
550646 if ( activeFilter ) {
551- const valueElement = document . querySelector ( `.filter-badge-value[data-id="${ activeFilter . id } "]` ) ;
647+ const valueId = `filter-value-${ activeFilter . id } ` ;
648+ const valueElement = document . getElementById ( valueId ) ;
552649 if ( valueElement ) {
553650 valueElement . focus ( ) ;
651+ lastFocusedElementId = valueId ;
554652 }
555653 }
556654}
@@ -560,20 +658,53 @@ <h1>Keyboard accessible filter prototype</h1>
560658 const valueInput = document . getElementById ( 'valueInput' ) ;
561659 if ( activeFilter ) {
562660 activeFilter . value = valueInput . value ;
563- const valueElement = document . querySelector ( `.filter-badge-value[data-id="${ activeFilter . id } "]` ) ;
661+
662+ // Update the display text without full re-render
663+ const valueId = `filter-value-${ activeFilter . id } ` ;
664+ const valueElement = document . getElementById ( valueId ) ;
564665 if ( valueElement ) {
565666 valueElement . textContent = activeFilter . value ;
667+ closeValuePopover ( ) ;
668+ valueElement . focus ( ) ;
669+ lastFocusedElementId = valueId ;
670+ } else {
671+ closeValuePopover ( ) ;
566672 }
567- closeValuePopover ( ) ;
568673 }
569674}
570675
571676// Remove filter
572677function removeFilter ( id ) {
573678 const index = filters . findIndex ( f => f . id === id ) ;
574679 if ( index !== - 1 ) {
680+ // Find next filter to focus on
681+ let nextFocusFilter ;
682+ if ( index < filters . length - 1 ) {
683+ nextFocusFilter = filters [ index + 1 ] ;
684+ } else if ( index > 0 ) {
685+ nextFocusFilter = filters [ index - 1 ] ;
686+ }
687+
688+ // Remove the filter
575689 filters . splice ( index , 1 ) ;
690+
691+ // Render and set focus
576692 renderFilterBadges ( ) ;
693+
694+ // If there are no filters left, focus the add button
695+ if ( filters . length === 0 ) {
696+ addFilterBadge . focus ( ) ;
697+ lastFocusedElementId = addFilterBadge . id ;
698+ }
699+ // Otherwise focus a related filter
700+ else if ( nextFocusFilter ) {
701+ const columnId = `filter-column-${ nextFocusFilter . id } ` ;
702+ const columnElement = document . getElementById ( columnId ) ;
703+ if ( columnElement ) {
704+ columnElement . focus ( ) ;
705+ lastFocusedElementId = columnId ;
706+ }
707+ }
577708 }
578709}
579710
0 commit comments