mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-30 18:22:37 -04:00 
			
		
		
		
	feat(web): keyboard access for search dropdown, combobox fixes (#8079)
* feat(web): keyboard access for search dropdown Also: fixing cosmetic issue with combobox component. * fix: revert changing required field * fix: create new focusChange action * fix: combobox usability improvements * handle escape key on the clear button * move focus to input when clear button is clicked * leave the dropdown closed if the user has already closed the dropdown and tabs over to the clear button * activate the combobox if a user tabs backwards onto the clear button * rename focusChange to focusOutside * small fixes * do not activate combobox on backwards tabbing * simplify classes in "No results" option * prevent dropdown option from being preselected when clear button is clicked * fix: remove unused event dispatcher interface
This commit is contained in:
		
							parent
							
								
									e21c96c0ef
								
							
						
					
					
						commit
						87ccba7f9d
					
				| @ -18,6 +18,7 @@ | ||||
|   import type { FormEventHandler } from 'svelte/elements'; | ||||
|   import { shortcuts } from '$lib/utils/shortcut'; | ||||
|   import { clickOutside } from '$lib/utils/click-outside'; | ||||
|   import { focusOutside } from '$lib/utils/focus-outside'; | ||||
| 
 | ||||
|   /** | ||||
|    * Unique identifier for the combobox. | ||||
| @ -40,6 +41,7 @@ | ||||
|   let searchQuery = selectedOption?.label || ''; | ||||
|   let selectedIndex: number | undefined; | ||||
|   let optionRefs: HTMLElement[] = []; | ||||
|   let input: HTMLInputElement; | ||||
|   const inputId = `combobox-${id}`; | ||||
|   const listboxId = `listbox-${id}`; | ||||
| 
 | ||||
| @ -51,7 +53,6 @@ | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ | ||||
|     select: ComboBoxOption | undefined; | ||||
|     click: void; | ||||
|   }>(); | ||||
| 
 | ||||
|   const activate = () => { | ||||
| @ -101,6 +102,8 @@ | ||||
|   }; | ||||
| 
 | ||||
|   const onClear = () => { | ||||
|     input?.focus(); | ||||
|     selectedIndex = undefined; | ||||
|     selectedOption = undefined; | ||||
|     searchQuery = ''; | ||||
|     dispatch('select', selectedOption); | ||||
| @ -111,11 +114,16 @@ | ||||
| <div | ||||
|   class="relative w-full dark:text-gray-300 text-gray-700 text-base" | ||||
|   use:clickOutside={{ onOutclick: deactivate }} | ||||
|   on:focusout={(e) => { | ||||
|     if (e.relatedTarget instanceof Node && !e.currentTarget.contains(e.relatedTarget)) { | ||||
|       deactivate(); | ||||
|     } | ||||
|   }} | ||||
|   use:focusOutside={{ onFocusOut: deactivate }} | ||||
|   use:shortcuts={[ | ||||
|     { | ||||
|       shortcut: { key: 'Escape' }, | ||||
|       onShortcut: (event) => { | ||||
|         event.stopPropagation(); | ||||
|         closeDropdown(); | ||||
|       }, | ||||
|     }, | ||||
|   ]} | ||||
| > | ||||
|   <div> | ||||
|     {#if isActive} | ||||
| @ -133,6 +141,7 @@ | ||||
|       aria-controls={listboxId} | ||||
|       aria-expanded={isOpen} | ||||
|       autocomplete="off" | ||||
|       bind:this={input} | ||||
|       class:!pl-8={isActive} | ||||
|       class:!rounded-b-none={isOpen} | ||||
|       class:cursor-pointer={!isActive} | ||||
| @ -213,9 +222,7 @@ | ||||
|           role="option" | ||||
|           aria-selected={selectedIndex === 0} | ||||
|           aria-disabled={true} | ||||
|           class:bg-gray-100={selectedIndex === 0} | ||||
|           class:dark:bg-gray-700={selectedIndex === 0} | ||||
|           class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default" | ||||
|           class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-100 aria-selected:dark:bg-gray-700" | ||||
|           id={`${listboxId}-${0}`} | ||||
|           on:click={() => closeDropdown()} | ||||
|         > | ||||
|  | ||||
| @ -12,6 +12,7 @@ | ||||
|   import { getMetadataSearchQuery } from '$lib/utils/metadata-search'; | ||||
|   import { handlePromiseError } from '$lib/utils'; | ||||
|   import { shortcut } from '$lib/utils/shortcut'; | ||||
|   import { focusOutside } from '$lib/utils/focus-outside'; | ||||
| 
 | ||||
|   export let value = ''; | ||||
|   export let grayTheme: boolean; | ||||
| @ -94,7 +95,7 @@ | ||||
|   }} | ||||
| /> | ||||
| 
 | ||||
| <div class="w-full relative" use:clickOutside={{ onOutclick: onFocusOut }}> | ||||
| <div class="w-full relative" use:clickOutside={{ onOutclick: onFocusOut }} use:focusOutside={{ onFocusOut }}> | ||||
|   <form | ||||
|     draggable="false" | ||||
|     autocomplete="off" | ||||
| @ -127,6 +128,7 @@ | ||||
|         bind:value | ||||
|         bind:this={input} | ||||
|         on:click={onFocusIn} | ||||
|         on:focus={onFocusIn} | ||||
|         disabled={showFilter} | ||||
|         use:shortcut={{ | ||||
|           shortcut: { key: 'Escape' }, | ||||
|  | ||||
							
								
								
									
										21
									
								
								web/src/lib/utils/focus-outside.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								web/src/lib/utils/focus-outside.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| interface Options { | ||||
|   onFocusOut?: () => void; | ||||
| } | ||||
| 
 | ||||
| export function focusOutside(node: HTMLElement, options: Options = {}) { | ||||
|   const { onFocusOut } = options; | ||||
| 
 | ||||
|   const handleFocusOut = (event: FocusEvent) => { | ||||
|     if (onFocusOut && event.relatedTarget instanceof Node && !node.contains(event.relatedTarget as Node)) { | ||||
|       onFocusOut(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   node.addEventListener('focusout', handleFocusOut); | ||||
| 
 | ||||
|   return { | ||||
|     destroy() { | ||||
|       node.removeEventListener('focusout', handleFocusOut); | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user