Files
svelte-headlessui/src/lib/components/listbox/ListboxOptions.svelte
Ryan Gossiaux 305dc1543f $api?. => $api. when possible
Now that this stuff has all been cleaned up with useXContext functions
2021-12-18 23:56:37 -08:00

121 lines
3.4 KiB
Svelte

<script lang="ts">
import { tick } from "svelte";
import { ListboxStates, useListboxContext } from "./Listbox.svelte";
import { useId } from "$lib/hooks/use-id";
import { match } from "$lib/utils/match";
import { Keys } from "$lib/utils/keyboard";
import { Focus } from "$lib/utils/calculate-active-index";
import { State, useOpenClosed } from "$lib/internal/open-closed";
let api = useListboxContext("ListboxOptions");
let id = `headlessui-listbox-options-${useId()}`;
let optionsRef = $api.optionsRef;
let buttonRef = $api.buttonRef;
let labelRef = $api.labelRef;
let searchDebounce: ReturnType<typeof setTimeout> | null = null;
async function handleKeyDown(event: KeyboardEvent) {
if (searchDebounce) clearTimeout(searchDebounce);
switch (event.key) {
// Ref: https://www.w3.org/TR/wai-aria-practices-1.2/#keyboard-interaction-12
case Keys.Space:
if ($api.searchQuery !== "") {
event.preventDefault();
event.stopPropagation();
return $api.search(event.key);
}
// When in type ahead mode, fallthrough
case Keys.Enter:
event.preventDefault();
event.stopPropagation();
if ($api.activeOptionIndex !== null) {
let { dataRef } = $api.options[$api.activeOptionIndex];
$api.select(dataRef.value);
}
$api.closeListbox();
await tick();
$buttonRef?.focus({ preventScroll: true });
break;
case match($api.orientation, {
vertical: Keys.ArrowDown,
horizontal: Keys.ArrowRight,
}):
event.preventDefault();
event.stopPropagation();
return $api.goToOption(Focus.Next);
case match($api.orientation, {
vertical: Keys.ArrowUp,
horizontal: Keys.ArrowLeft,
}):
event.preventDefault();
event.stopPropagation();
return $api.goToOption(Focus.Previous);
case Keys.Home:
case Keys.PageUp:
event.preventDefault();
event.stopPropagation();
return $api.goToOption(Focus.First);
case Keys.End:
case Keys.PageDown:
event.preventDefault();
event.stopPropagation();
return $api.goToOption(Focus.Last);
case Keys.Escape:
event.preventDefault();
event.stopPropagation();
$api.closeListbox();
await tick();
$buttonRef?.focus({ preventScroll: true });
break;
case Keys.Tab:
event.preventDefault();
event.stopPropagation();
break;
default:
if (event.key.length === 1) {
$api.search(event.key);
searchDebounce = setTimeout(() => $api.clearSearch(), 350);
}
break;
}
}
$: propsWeControl = {
"aria-activedescendant":
$api.activeOptionIndex === null
? undefined
: $api.options[$api.activeOptionIndex]?.id,
"aria-labelledby": $labelRef?.id ?? $buttonRef?.id,
"aria-orientation": $api.orientation,
id,
role: "listbox",
tabIndex: 0,
};
let usesOpenClosedState = useOpenClosed();
$: visible =
usesOpenClosedState !== undefined
? $usesOpenClosedState === State.Open
: $api.listboxState === ListboxStates.Open;
</script>
{#if visible}
<ul
bind:this={$optionsRef}
on:keydown={handleKeyDown}
{...$$restProps}
{...propsWeControl}
>
<slot open={$api.listboxState === ListboxStates.Open} />
</ul>
{/if}