Skip to content

Commit 1cb3f29

Browse files
committed
arrow navigation in suggestions
1 parent f73466d commit 1cb3f29

File tree

1 file changed

+55
-19
lines changed

1 file changed

+55
-19
lines changed

src/components/Selection.svelte

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
2323
let item = $state('')
2424
let show_suggestions = $state(false)
25+
let active_index = $state(0)
26+
27+
const id = $props.id()
2528
2629
let suggestions = $derived(
2730
selected_items.length >= max ? [] : allowed_items.filter(is_suggestion),
@@ -49,18 +52,14 @@
4952
if (!is_valid(item)) return
5053
selected_items.push(item.trim())
5154
item = ''
55+
active_index = 0
5256
}
5357
5458
function select(allowed_item: string) {
5559
selected_items.push(allowed_item)
5660
item = ''
5761
show_suggestions = false
58-
}
59-
60-
function handle_keydown(e: KeyboardEvent) {
61-
if (show_suggestions && e.key === 'Escape') {
62-
show_suggestions = false
63-
}
62+
active_index = 0
6463
}
6564
6665
function handle_blur(e: FocusEvent) {
@@ -79,14 +78,45 @@
7978
} else {
8079
show_suggestions = true
8180
}
81+
82+
active_index = 0
8283
}
8384
8485
function remove_item(item: string) {
8586
selected_items = selected_items.filter((_item) => _item !== item)
8687
}
87-
</script>
8888
89-
<svelte:window onkeydown={handle_keydown} />
89+
function handle_keydown(e: KeyboardEvent) {
90+
const key = e.key
91+
92+
switch (key) {
93+
case 'Escape':
94+
if (show_suggestions) show_suggestions = false
95+
break
96+
case 'Enter':
97+
select(suggestions[active_index])
98+
break
99+
case 'ArrowUp':
100+
if (active_index > 0) {
101+
active_index--
102+
scroll_to_option()
103+
}
104+
break
105+
case 'ArrowDown':
106+
if (active_index < suggestions.length - 1) {
107+
active_index++
108+
scroll_to_option()
109+
}
110+
break
111+
}
112+
}
113+
114+
function scroll_to_option() {
115+
document.querySelector(`#${id}-${active_index}`)?.scrollIntoView({
116+
block: 'center',
117+
})
118+
}
119+
</script>
90120

91121
<section aria-label={section_label}>
92122
{#if title}
@@ -102,14 +132,21 @@
102132
bind:value={item}
103133
onfocus={() => (show_suggestions = true)}
104134
oninput={handle_input}
135+
onkeydown={handle_keydown}
105136
onblur={handle_blur}
106137
/>
107138
</div>
108139

109140
{#if show_suggestions && suggestions.length > 0}
110-
<div class="suggestions" bind:this={suggestions_element}>
111-
{#each suggestions as allowed_item}
112-
<button onclick={() => select(allowed_item)}>
141+
<div class="suggestions" bind:this={suggestions_element} tabindex="-1">
142+
{#each suggestions as allowed_item, i}
143+
<button
144+
id="{id}-{i}"
145+
tabindex="-1"
146+
class="option"
147+
class:selected={i === active_index}
148+
onclick={() => select(allowed_item)}
149+
>
113150
{allowed_item}
114151
</button>
115152
{/each}
@@ -159,16 +196,15 @@
159196
border-radius: 0.4rem;
160197
box-shadow: 0 0 1rem var(--shadow-color);
161198
display: grid;
199+
}
162200
163-
button {
164-
font-size: 1rem;
165-
text-align: left;
166-
padding: 0.25rem 1rem;
201+
.option {
202+
font-size: 1rem;
203+
text-align: left;
204+
padding: 0.25rem 1rem;
167205
168-
&:hover,
169-
&:focus-visible {
170-
background-color: var(--secondary-bg-color);
171-
}
206+
&.selected {
207+
background-color: var(--secondary-bg-color);
172208
}
173209
}
174210
</style>

0 commit comments

Comments
 (0)