|
22 | 22 |
|
23 | 23 | let item = $state('') |
24 | 24 | let show_suggestions = $state(false) |
| 25 | + let active_index = $state(0) |
| 26 | +
|
| 27 | + const id = $props.id() |
25 | 28 |
|
26 | 29 | let suggestions = $derived( |
27 | 30 | selected_items.length >= max ? [] : allowed_items.filter(is_suggestion), |
|
49 | 52 | if (!is_valid(item)) return |
50 | 53 | selected_items.push(item.trim()) |
51 | 54 | item = '' |
| 55 | + active_index = 0 |
52 | 56 | } |
53 | 57 |
|
54 | 58 | function select(allowed_item: string) { |
55 | 59 | selected_items.push(allowed_item) |
56 | 60 | item = '' |
57 | 61 | 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 |
64 | 63 | } |
65 | 64 |
|
66 | 65 | function handle_blur(e: FocusEvent) { |
|
79 | 78 | } else { |
80 | 79 | show_suggestions = true |
81 | 80 | } |
| 81 | +
|
| 82 | + active_index = 0 |
82 | 83 | } |
83 | 84 |
|
84 | 85 | function remove_item(item: string) { |
85 | 86 | selected_items = selected_items.filter((_item) => _item !== item) |
86 | 87 | } |
87 | | -</script> |
88 | 88 |
|
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> |
90 | 120 |
|
91 | 121 | <section aria-label={section_label}> |
92 | 122 | {#if title} |
|
102 | 132 | bind:value={item} |
103 | 133 | onfocus={() => (show_suggestions = true)} |
104 | 134 | oninput={handle_input} |
| 135 | + onkeydown={handle_keydown} |
105 | 136 | onblur={handle_blur} |
106 | 137 | /> |
107 | 138 | </div> |
108 | 139 |
|
109 | 140 | {#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 | + > |
113 | 150 | {allowed_item} |
114 | 151 | </button> |
115 | 152 | {/each} |
|
159 | 196 | border-radius: 0.4rem; |
160 | 197 | box-shadow: 0 0 1rem var(--shadow-color); |
161 | 198 | display: grid; |
| 199 | + } |
162 | 200 |
|
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; |
167 | 205 |
|
168 | | - &:hover, |
169 | | - &:focus-visible { |
170 | | - background-color: var(--secondary-bg-color); |
171 | | - } |
| 206 | + &.selected { |
| 207 | + background-color: var(--secondary-bg-color); |
172 | 208 | } |
173 | 209 | } |
174 | 210 | </style> |
0 commit comments