feat(web): combobox accessibility improvements (#8007)

* bump skip link z index, to prevent overlap with the search box

* combobox refactor initial commit

* pull label into the combobox component

* feat(web): combobox accessibility improvements

* fix: replace crypto.randomUUID, fix border UI bug, simpler focus handling (#2)

* fix: handle changes in the selected option

* fix: better escape key handling in search bar

* fix: remove broken tailwind classes

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* fix: remove custom "outclick" handler logic

* fix: use focusout instead of custom key handlers to detect focus change

* fix: move escape key handling to the window

Also add escape key handling to the input box, to make sure that the "recent searches" dropdown gets closed too.

* fix: better input event handling

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* fix: highlighting selected dropdown element

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
This commit is contained in:
Ben Basten 2024-03-19 12:56:41 +00:00 committed by GitHub
parent 033f83a55a
commit c6d2408517
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 214 additions and 79 deletions

View file

@ -11,6 +11,7 @@
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
import { handlePromiseError } from '$lib/utils';
import { shortcut } from '$lib/utils/shortcut';
export let value = '';
export let grayTheme: boolean;
@ -84,7 +85,16 @@
};
</script>
<div class="w-full relative" use:clickOutside on:outclick={onFocusOut} on:escape={onFocusOut}>
<svelte:window
use:shortcut={{
shortcut: { key: 'Escape' },
onShortcut: () => {
onFocusOut();
},
}}
/>
<div class="w-full relative" use:clickOutside={{ onOutclick: onFocusOut }}>
<form
draggable="false"
autocomplete="off"
@ -118,6 +128,12 @@
bind:this={input}
on:click={onFocusIn}
disabled={showFilter}
use:shortcut={{
shortcut: { key: 'Escape' },
onShortcut: () => {
onFocusOut();
},
}}
/>
<div class="absolute inset-y-0 {showClearIcon ? 'right-14' : 'right-5'} flex items-center pl-6 transition-all">

View file

@ -40,24 +40,24 @@
<div class="grid grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] gap-5 mt-1">
<div class="w-full">
<label class="text-sm text-black dark:text-white" for="search-camera-make">Make</label>
<Combobox
id="search-camera-make"
options={toComboBoxOptions(makes)}
selectedOption={makeFilter ? { label: makeFilter, value: makeFilter } : undefined}
id="camera-make"
label="Make"
on:select={({ detail }) => (filters.make = detail?.value)}
options={toComboBoxOptions(makes)}
placeholder="Search camera make..."
selectedOption={makeFilter ? { label: makeFilter, value: makeFilter } : undefined}
/>
</div>
<div class="w-full">
<label class="text-sm text-black dark:text-white" for="search-camera-model">Model</label>
<Combobox
id="search-camera-model"
options={toComboBoxOptions(models)}
selectedOption={modelFilter ? { label: modelFilter, value: modelFilter } : undefined}
id="camera-model"
label="Model"
on:select={({ detail }) => (filters.model = detail?.value)}
options={toComboBoxOptions(models)}
placeholder="Search camera model..."
selectedOption={modelFilter ? { label: modelFilter, value: modelFilter } : undefined}
/>
</div>
</div>

View file

@ -62,35 +62,35 @@
<div class="grid grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] gap-5 mt-1">
<div class="w-full">
<label class="text-sm text-black dark:text-white" for="search-place-country">Country</label>
<Combobox
id="search-place-country"
options={toComboBoxOptions(countries)}
selectedOption={filters.country ? { label: filters.country, value: filters.country } : undefined}
id="location-country"
label="Country"
on:select={({ detail }) => (filters.country = detail?.value)}
options={toComboBoxOptions(countries)}
placeholder="Search country..."
selectedOption={filters.country ? { label: filters.country, value: filters.country } : undefined}
/>
</div>
<div class="w-full">
<label class="text-sm text-black dark:text-white" for="search-place-state">State</label>
<Combobox
id="search-place-state"
options={toComboBoxOptions(states)}
selectedOption={filters.state ? { label: filters.state, value: filters.state } : undefined}
id="location-state"
label="State"
on:select={({ detail }) => (filters.state = detail?.value)}
options={toComboBoxOptions(states)}
placeholder="Search state..."
selectedOption={filters.state ? { label: filters.state, value: filters.state } : undefined}
/>
</div>
<div class="w-full">
<label class="text-sm text-black dark:text-white" for="search-place-city">City</label>
<Combobox
id="search-place-city"
options={toComboBoxOptions(cities)}
selectedOption={filters.city ? { label: filters.city, value: filters.city } : undefined}
id="location-city"
label="City"
on:select={({ detail }) => (filters.city = detail?.value)}
options={toComboBoxOptions(cities)}
placeholder="Search city..."
selectedOption={filters.city ? { label: filters.city, value: filters.city } : undefined}
/>
</div>
</div>