aboutsummaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/CommandPalette/CommandPalette.svelte50
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>