...
This commit is contained in:
123
src/lib/components/listbox/Listbox.svelte
Normal file
123
src/lib/components/listbox/Listbox.svelte
Normal file
@@ -0,0 +1,123 @@
|
||||
<script lang="ts" generics="T">
|
||||
import ListboxItem from './ListboxItem.svelte';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { setListboxContext, type ListboxItemAbstract } from './context';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
/** @description Determine if multiple items can be selected */
|
||||
export let allowMultiple = true;
|
||||
|
||||
/** @description Determine if multiple items can be selected */
|
||||
export let items: T[];
|
||||
|
||||
/** @description Determine if multiple items can be selected */
|
||||
export let defaultSelectedItems: T[];
|
||||
|
||||
/** @description Function called when the selected items changes */
|
||||
export let onChange: (items: T[]) => void;
|
||||
|
||||
let selectedItems: Writable<ListboxItemAbstract<T>[]> = writable([]);
|
||||
let registeredItems: Writable<ListboxItemAbstract<T>[]> = writable([]);
|
||||
let focusedItem: Writable<ListboxItemAbstract<T> | null> = writable(null);
|
||||
|
||||
function toggleItemSelected(item: ListboxItemAbstract<T>): void {
|
||||
if (item) {
|
||||
setFocus(item);
|
||||
if ($selectedItems.includes(item)) {
|
||||
if (allowMultiple) {
|
||||
$selectedItems = $selectedItems.filter((x) => x !== item);
|
||||
} else {
|
||||
$selectedItems = [];
|
||||
}
|
||||
} else {
|
||||
if (allowMultiple) {
|
||||
$selectedItems.push(item);
|
||||
$selectedItems = $selectedItems;
|
||||
} else {
|
||||
$selectedItems = [item];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onChange($selectedItems.map((x) => x.item));
|
||||
}
|
||||
|
||||
function setFocus(item: ListboxItemAbstract<T>) {
|
||||
const itemEl = document.getElementById(item.id);
|
||||
$focusedItem = item;
|
||||
itemEl?.focus();
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent, item: ListboxItemAbstract<T>) {
|
||||
const index = $registeredItems.findIndex((x) => x === item);
|
||||
if (index === -1) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
if ($registeredItems.length - 1 === index) {
|
||||
setFocus($registeredItems[0]);
|
||||
} else {
|
||||
setFocus($registeredItems[index + 1]);
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
if (index === 0) {
|
||||
setFocus($registeredItems[$registeredItems.length - 1]);
|
||||
} else {
|
||||
setFocus($registeredItems[index - 1]);
|
||||
}
|
||||
break;
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
e.preventDefault();
|
||||
toggleItemSelected(item);
|
||||
break;
|
||||
case 'Home':
|
||||
setFocus($registeredItems[0]);
|
||||
break;
|
||||
case 'End':
|
||||
setFocus($registeredItems[$registeredItems.length - 1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function registerItem(item: ListboxItemAbstract<T>): void {
|
||||
$registeredItems.push(item);
|
||||
$registeredItems = $registeredItems;
|
||||
if (defaultSelectedItems.includes(item.item)) {
|
||||
$selectedItems.push(item);
|
||||
$selectedItems = $selectedItems;
|
||||
}
|
||||
if ($registeredItems.length === 1) {
|
||||
$focusedItem = $registeredItems[0];
|
||||
}
|
||||
}
|
||||
|
||||
function unregisterItem(item: ListboxItemAbstract<T>): void {
|
||||
$registeredItems = $registeredItems.filter((x) => x.id !== item.id);
|
||||
$selectedItems = $selectedItems.filter((x) => x.id !== item.id);
|
||||
}
|
||||
|
||||
setListboxContext<T>({
|
||||
registerItem: registerItem,
|
||||
unregisterItem: unregisterItem,
|
||||
selectedItems: selectedItems,
|
||||
focusedItem: focusedItem
|
||||
});
|
||||
</script>
|
||||
|
||||
<ul
|
||||
class={twMerge('flex flex-col gap-2 p-4', $$restProps['class'])}
|
||||
role="listbox"
|
||||
aria-activedescendant={$focusedItem?.id}
|
||||
aria-multiselectable={allowMultiple}
|
||||
tabindex={0}
|
||||
>
|
||||
{#each items as item}
|
||||
<ListboxItem {onKeyDown} onClick={(e, x) => toggleItemSelected(x)} {item}>
|
||||
<slot {item} />
|
||||
</ListboxItem>
|
||||
{/each}
|
||||
</ul>
|
||||
Reference in New Issue
Block a user