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 type { FormEventHandler } from 'svelte/elements';
|
||||||
import { shortcuts } from '$lib/utils/shortcut';
|
import { shortcuts } from '$lib/utils/shortcut';
|
||||||
import { clickOutside } from '$lib/utils/click-outside';
|
import { clickOutside } from '$lib/utils/click-outside';
|
||||||
|
import { focusOutside } from '$lib/utils/focus-outside';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unique identifier for the combobox.
|
* Unique identifier for the combobox.
|
||||||
@ -40,6 +41,7 @@
|
|||||||
let searchQuery = selectedOption?.label || '';
|
let searchQuery = selectedOption?.label || '';
|
||||||
let selectedIndex: number | undefined;
|
let selectedIndex: number | undefined;
|
||||||
let optionRefs: HTMLElement[] = [];
|
let optionRefs: HTMLElement[] = [];
|
||||||
|
let input: HTMLInputElement;
|
||||||
const inputId = `combobox-${id}`;
|
const inputId = `combobox-${id}`;
|
||||||
const listboxId = `listbox-${id}`;
|
const listboxId = `listbox-${id}`;
|
||||||
|
|
||||||
@ -51,7 +53,6 @@
|
|||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
select: ComboBoxOption | undefined;
|
select: ComboBoxOption | undefined;
|
||||||
click: void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const activate = () => {
|
const activate = () => {
|
||||||
@ -101,6 +102,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onClear = () => {
|
const onClear = () => {
|
||||||
|
input?.focus();
|
||||||
|
selectedIndex = undefined;
|
||||||
selectedOption = undefined;
|
selectedOption = undefined;
|
||||||
searchQuery = '';
|
searchQuery = '';
|
||||||
dispatch('select', selectedOption);
|
dispatch('select', selectedOption);
|
||||||
@ -111,11 +114,16 @@
|
|||||||
<div
|
<div
|
||||||
class="relative w-full dark:text-gray-300 text-gray-700 text-base"
|
class="relative w-full dark:text-gray-300 text-gray-700 text-base"
|
||||||
use:clickOutside={{ onOutclick: deactivate }}
|
use:clickOutside={{ onOutclick: deactivate }}
|
||||||
on:focusout={(e) => {
|
use:focusOutside={{ onFocusOut: deactivate }}
|
||||||
if (e.relatedTarget instanceof Node && !e.currentTarget.contains(e.relatedTarget)) {
|
use:shortcuts={[
|
||||||
deactivate();
|
{
|
||||||
}
|
shortcut: { key: 'Escape' },
|
||||||
}}
|
onShortcut: (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
closeDropdown();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{#if isActive}
|
{#if isActive}
|
||||||
@ -133,6 +141,7 @@
|
|||||||
aria-controls={listboxId}
|
aria-controls={listboxId}
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
bind:this={input}
|
||||||
class:!pl-8={isActive}
|
class:!pl-8={isActive}
|
||||||
class:!rounded-b-none={isOpen}
|
class:!rounded-b-none={isOpen}
|
||||||
class:cursor-pointer={!isActive}
|
class:cursor-pointer={!isActive}
|
||||||
@ -213,9 +222,7 @@
|
|||||||
role="option"
|
role="option"
|
||||||
aria-selected={selectedIndex === 0}
|
aria-selected={selectedIndex === 0}
|
||||||
aria-disabled={true}
|
aria-disabled={true}
|
||||||
class:bg-gray-100={selectedIndex === 0}
|
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"
|
||||||
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"
|
|
||||||
id={`${listboxId}-${0}`}
|
id={`${listboxId}-${0}`}
|
||||||
on:click={() => closeDropdown()}
|
on:click={() => closeDropdown()}
|
||||||
>
|
>
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { shortcut } from '$lib/utils/shortcut';
|
import { shortcut } from '$lib/utils/shortcut';
|
||||||
|
import { focusOutside } from '$lib/utils/focus-outside';
|
||||||
|
|
||||||
export let value = '';
|
export let value = '';
|
||||||
export let grayTheme: boolean;
|
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
|
<form
|
||||||
draggable="false"
|
draggable="false"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@ -127,6 +128,7 @@
|
|||||||
bind:value
|
bind:value
|
||||||
bind:this={input}
|
bind:this={input}
|
||||||
on:click={onFocusIn}
|
on:click={onFocusIn}
|
||||||
|
on:focus={onFocusIn}
|
||||||
disabled={showFilter}
|
disabled={showFilter}
|
||||||
use:shortcut={{
|
use:shortcut={{
|
||||||
shortcut: { key: 'Escape' },
|
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