mirror of
https://github.com/immich-app/immich.git
synced 2024-12-22 01:47:08 +02: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…
Reference in New Issue
Block a user