diff options
Diffstat (limited to 'src/lib/CommandPalette/CommandPalette.svelte')
| -rw-r--r-- | src/lib/CommandPalette/CommandPalette.svelte | 50 |
1 files changed, 47 insertions, 3 deletions
diff --git a/src/lib/CommandPalette/CommandPalette.svelte b/src/lib/CommandPalette/CommandPalette.svelte index 8ace0222..5eb06ef7 100644 --- a/src/lib/CommandPalette/CommandPalette.svelte +++ b/src/lib/CommandPalette/CommandPalette.svelte @@ -14,6 +14,10 @@ let inputRef: HTMLInputElement; let isVisible = false; let timeoutID: ReturnType<typeof setTimeout> | null = null; let itemIDs = new Map<string, number>(); +let previouslyFocused: HTMLElement | null = null; +const instanceId = `cmd-palette-${Math.random().toString(36).slice(2, 10)}`; +const listboxId = `${instanceId}-listbox`; +const optionDomId = (index: number) => `${instanceId}-option-${index}`; $: { items.forEach((item, index) => { @@ -63,6 +67,23 @@ $: { $: if (selectedIndex >= filtered.length) selectedIndex = filtered.length - 1; $: if (selectedIndex < 0 && filtered.length > 0) selectedIndex = 0; +$: activeOptionId = + selectedIndex >= 0 && selectedIndex < filtered.length + ? optionDomId(selectedIndex) + : undefined; + +$: if (open && !previouslyFocused) { + previouslyFocused = document.activeElement as HTMLElement | null; +} + +$: if (!open && previouslyFocused) { + const toRestore = previouslyFocused; + + previouslyFocused = null; + + requestAnimationFrame(() => toRestore?.focus()); +} + $: if (open && !isVisible) { isVisible = true; @@ -103,7 +124,10 @@ const handleKey = (e: KeyboardEvent) => { executeItem(filtered[selectedIndex]); } } else if (e.key === "Escape") { + e.preventDefault(); open = false; + } else if (e.key === "Tab") { + e.preventDefault(); } }; @@ -145,23 +169,41 @@ const handleGlobalKey = (e: KeyboardEvent) => { <div class="command-palette-overlay {open ? 'fade-in' : 'fade-out'}" onclick={() => (open = false)} + aria-hidden="true" ></div> - <div class="dropdown {open ? 'fade-in' : 'fade-out'}"> + <div + class="dropdown {open ? 'fade-in' : 'fade-out'}" + role="dialog" + aria-modal="true" + aria-label="Command palette" + > <div class="dropdown-content card card-small"> <input bind:this={inputRef} bind:value={search} class="command-input" + type="text" placeholder="Search" + aria-label="Search commands" + autocomplete="off" + spellcheck="false" + role="combobox" + aria-expanded={open} + aria-controls={listboxId} + aria-autocomplete="list" + aria-activedescendant={activeOptionId} onkeydown={handleKey} /> - <div class="results-container"> + <div class="results-container" role="listbox" id={listboxId} aria-label="Commands"> {#each filtered as item, i (item.id || item.url)} <a + id={optionDomId(i)} href={item.url} class="header-item {selectedIndex === i ? 'selected' : ''}" + role="option" + aria-selected={selectedIndex === i} in:fly={{ y: 20, duration: 150, delay: i * 30 }} out:fly={{ y: -20, duration: 150 }} animate:flip={{ duration: 200 }} @@ -177,7 +219,9 @@ const handleGlobalKey = (e: KeyboardEvent) => { {/each} {#if filtered.length === 0 && search !== ''} - <div class="no-results opaque" in:fade={{ duration: 150 }}>No results found</div> + <div class="no-results opaque" role="status" in:fade={{ duration: 150 }}> + No results found + </div> {/if} </div> </div> |