Spaces:
Build error
Build error
File size: 3,312 Bytes
87a665c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | <script lang="ts">
import { flyAndScale } from '$lib/utils/transitions';
import { tick } from 'svelte';
/** Currently selected value */
export let value = '';
/** Items array: { value: string, label: string }[] */
export let items = [];
/** Placeholder text when no value is selected */
export let placeholder = '';
/** Callback when value changes */
export let onChange: (value: string) => void = () => {};
/** CSS classes for the trigger button */
export let triggerClass = '';
/** CSS classes for the label inside the trigger */
export let labelClass = '';
/** CSS classes for the dropdown content container */
export let contentClass =
'rounded-2xl min-w-[170px] p-1 border border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-850 dark:text-white shadow-lg';
/** CSS classes for each item button */
export let itemClass =
'flex w-full gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl';
/** Alignment of the dropdown: 'start' | 'end' */
export let align = 'start';
/** Callback when dropdown closes */
export let onClose: () => void = () => {};
export let open = false;
let triggerEl;
let contentEl;
$: selectedLabel = items.find((i) => i.value === value)?.label ?? placeholder;
/** Svelte action: moves the node to document.body (portal) */
function portal(node) {
document.body.appendChild(node);
return {
destroy() {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
}
};
}
function positionContent() {
if (!triggerEl || !contentEl) return;
const rect = triggerEl.getBoundingClientRect();
contentEl.style.position = 'fixed';
contentEl.style.zIndex = '9999';
contentEl.style.top = `${rect.bottom + 4}px`;
contentEl.style.minWidth = `${rect.width}px`;
if (align === 'end') {
contentEl.style.right = `${window.innerWidth - rect.right}px`;
contentEl.style.left = 'auto';
} else {
contentEl.style.left = `${rect.left}px`;
contentEl.style.right = 'auto';
}
}
async function toggleOpen() {
open = !open;
if (open) {
await tick();
positionContent();
}
}
function handleWindowClick(event) {
if (!open) return;
if (triggerEl?.contains(event.target)) return;
if (contentEl?.contains(event.target)) return;
open = false;
onClose();
}
function handleKeydown(event) {
if (event.key === 'Escape' && open) {
open = false;
onClose();
}
}
export function selectItem(item) {
value = item.value;
open = false;
onChange(value);
}
</script>
<svelte:window
on:click={handleWindowClick}
on:keydown={handleKeydown}
on:scroll|capture={positionContent}
on:resize={positionContent}
/>
<button
bind:this={triggerEl}
class={triggerClass}
aria-label={placeholder}
type="button"
on:click={toggleOpen}
>
<slot name="trigger" {selectedLabel} {open}>
<span class={labelClass}>
{selectedLabel}
</span>
</slot>
</button>
{#if open}
<div use:portal bind:this={contentEl} class={contentClass} transition:flyAndScale>
<slot {open} {selectItem}>
{#each items as item}
<button class={itemClass} type="button" on:click={() => selectItem(item)}>
<slot name="item" {item} selected={value === item.value}>
{item.label}
</slot>
</button>
{/each}
</slot>
</div>
{/if}
|