From c6d24085176b9da263ed0d3ad659267cc802ce8d Mon Sep 17 00:00:00 2001 From: Ben Basten <45583362+ben-basten@users.noreply.github.com> Date: Tue, 19 Mar 2024 12:56:41 +0000 Subject: [PATCH] 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> --- .../elements/buttons/skip-link.svelte | 2 +- .../shared-components/change-date.svelte | 9 +- .../shared-components/combobox.svelte | 219 +++++++++++++----- .../search-bar/search-bar.svelte | 18 +- .../search-bar/search-camera-section.svelte | 16 +- .../search-bar/search-location-section.svelte | 24 +- .../settings/setting-combobox.svelte | 4 + .../user-settings-page/app-settings.svelte | 1 + 8 files changed, 214 insertions(+), 79 deletions(-) diff --git a/web/src/lib/components/elements/buttons/skip-link.svelte b/web/src/lib/components/elements/buttons/skip-link.svelte index 457661d65f..8a304469d7 100644 --- a/web/src/lib/components/elements/buttons/skip-link.svelte +++ b/web/src/lib/components/elements/buttons/skip-link.svelte @@ -14,7 +14,7 @@ }; -
+
- - +
diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index c69460640b..19fd73d25f 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -11,48 +11,93 @@ -
+ +
{ + if (e.relatedTarget instanceof Node && !e.currentTarget.contains(e.relatedTarget)) { + deactivate(); + } + }} +>
- {#if isOpen} + {#if isActive}
- +
{/if} (searchQuery = e.currentTarget.value)} - on:focus={handleClick} - on:blur={() => (inputFocused = false)} + class:cursor-pointer={!isActive} + class="immich-form-input text-sm text-left w-full !pr-12 transition-all" + id={inputId} + on:click={activate} + on:focus={activate} + on:input={onInput} + role="combobox" + type="text" + value={searchQuery} + use:shortcuts={[ + { + shortcut: { key: 'ArrowUp' }, + onShortcut: () => { + openDropdown(); + void incrementSelectedIndex(-1); + }, + }, + { + shortcut: { key: 'ArrowDown' }, + onShortcut: () => { + openDropdown(); + void incrementSelectedIndex(1); + }, + }, + { + shortcut: { key: 'ArrowDown', alt: true }, + onShortcut: () => { + openDropdown(); + }, + }, + { + shortcut: { key: 'Enter' }, + onShortcut: () => { + if (selectedIndex !== undefined && filteredOptions.length > 0) { + onSelect(filteredOptions[selectedIndex]); + } + closeDropdown(); + }, + }, + { + shortcut: { key: 'Escape' }, + onShortcut: () => { + closeDropdown(); + }, + }, + ]} />
{#if selectedOption} - + {:else if !isOpen} - + {/if}
- {#if isOpen} -
+
    + {#if isOpen} {#if filteredOptions.length === 0} -
    No results
    - {/if} - {#each filteredOptions as option (option.label)} - {@const selected = option.label === selectedOption?.label} - + {/each} -
- {/if} + {/if} +
diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte index 041181f71b..8afa56df71 100644 --- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte @@ -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 @@ }; -
+ { + onFocusOut(); + }, + }} +/> + +
{ + onFocusOut(); + }, + }} />
diff --git a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte index 6f377faffb..7acac54d8e 100644 --- a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte @@ -40,24 +40,24 @@
- (filters.make = detail?.value)} + options={toComboBoxOptions(makes)} placeholder="Search camera make..." + selectedOption={makeFilter ? { label: makeFilter, value: makeFilter } : undefined} />
- (filters.model = detail?.value)} + options={toComboBoxOptions(models)} placeholder="Search camera model..." + selectedOption={modelFilter ? { label: modelFilter, value: modelFilter } : undefined} />
diff --git a/web/src/lib/components/shared-components/search-bar/search-location-section.svelte b/web/src/lib/components/shared-components/search-bar/search-location-section.svelte index ac412c5138..fdaabe0f75 100644 --- a/web/src/lib/components/shared-components/search-bar/search-location-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-location-section.svelte @@ -62,35 +62,35 @@
- (filters.country = detail?.value)} + options={toComboBoxOptions(countries)} placeholder="Search country..." + selectedOption={filters.country ? { label: filters.country, value: filters.country } : undefined} />
- (filters.state = detail?.value)} + options={toComboBoxOptions(states)} placeholder="Search state..." + selectedOption={filters.state ? { label: filters.state, value: filters.state } : undefined} />
- (filters.city = detail?.value)} + options={toComboBoxOptions(cities)} placeholder="Search city..." + selectedOption={filters.city ? { label: filters.city, value: filters.city } : undefined} />
diff --git a/web/src/lib/components/shared-components/settings/setting-combobox.svelte b/web/src/lib/components/shared-components/settings/setting-combobox.svelte index 7f3dc1906e..ee396935c6 100644 --- a/web/src/lib/components/shared-components/settings/setting-combobox.svelte +++ b/web/src/lib/components/shared-components/settings/setting-combobox.svelte @@ -3,6 +3,7 @@ import { fly } from 'svelte/transition'; import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte'; + export let id: string; export let title: string; export let comboboxPlaceholder: string; export let subtitle = ''; @@ -32,6 +33,9 @@