91 lines
2.1 KiB
Svelte
91 lines
2.1 KiB
Svelte
|
|
<script lang="ts">
|
||
|
|
type CssClass = string | (string | false | null | undefined)[];
|
||
|
|
|
||
|
|
type Props = {
|
||
|
|
completions: string[];
|
||
|
|
popover_class?: CssClass;
|
||
|
|
search_input_class?: CssClass;
|
||
|
|
search_input_element?: HTMLInputElement;
|
||
|
|
search_value: string;
|
||
|
|
value: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
let {
|
||
|
|
completions,
|
||
|
|
popover_class,
|
||
|
|
search_input_class,
|
||
|
|
search_input_element = $bindable(),
|
||
|
|
search_value = $bindable(),
|
||
|
|
value = $bindable(),
|
||
|
|
}: Props = $props();
|
||
|
|
|
||
|
|
let focused = $state(false);
|
||
|
|
let popover_element = $state<HTMLDivElement | undefined>();
|
||
|
|
|
||
|
|
function handle_component_focusin() {
|
||
|
|
focused = true;
|
||
|
|
popover_element?.showPopover();
|
||
|
|
}
|
||
|
|
|
||
|
|
function handle_component_focusout() {
|
||
|
|
focused = false;
|
||
|
|
setTimeout(() => {
|
||
|
|
// TODO: There's still an edge case, where a click with a
|
||
|
|
// mousedown-to-mouseup duration greater than the delay here will cause
|
||
|
|
// the popover to hide.
|
||
|
|
if (!focused) {
|
||
|
|
popover_element?.hidePopover();
|
||
|
|
}
|
||
|
|
}, 250);
|
||
|
|
}
|
||
|
|
|
||
|
|
function handle_completion_click(completion: string) {
|
||
|
|
value = completion;
|
||
|
|
search_input_element?.focus();
|
||
|
|
popover_element?.hidePopover();
|
||
|
|
}
|
||
|
|
|
||
|
|
function handle_search_keydown(ev: KeyboardEvent) {
|
||
|
|
if (ev.key === "Escape") {
|
||
|
|
popover_element?.hidePopover();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function handle_search_input() {
|
||
|
|
popover_element?.showPopover();
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<div
|
||
|
|
class="combobox__container"
|
||
|
|
onfocusin={handle_component_focusin}
|
||
|
|
onfocusout={handle_component_focusout}
|
||
|
|
>
|
||
|
|
<input
|
||
|
|
bind:this={search_input_element}
|
||
|
|
bind:value={search_value}
|
||
|
|
class={search_input_class}
|
||
|
|
oninput={handle_search_input}
|
||
|
|
onkeydown={handle_search_keydown}
|
||
|
|
type="text"
|
||
|
|
/>
|
||
|
|
<div
|
||
|
|
bind:this={popover_element}
|
||
|
|
class={popover_class ?? "combobox__popover"}
|
||
|
|
popover="manual"
|
||
|
|
role="listbox"
|
||
|
|
>
|
||
|
|
{#each completions as completion}
|
||
|
|
<button
|
||
|
|
aria-selected={value === completion}
|
||
|
|
class="combobox__completion"
|
||
|
|
onclick={() => handle_completion_click(completion)}
|
||
|
|
role="option"
|
||
|
|
type="button"
|
||
|
|
>
|
||
|
|
{completion}
|
||
|
|
</button>
|
||
|
|
{/each}
|
||
|
|
</div>
|
||
|
|
</div>
|