| <!-- |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| |
| --> |
| <script lang="ts"> |
| import { createEventDispatcher } from 'svelte'; |
| import { createListbox } from 'svelte-headlessui'; |
| import { fade } from 'svelte/transition'; |
| import Icon from './Icon.svelte'; |
| import { twMerge } from 'tailwind-merge'; |
| |
| type GenericOption = { value: string; name: string }; |
| |
| interface Props<T extends GenericOption> { |
| options: readonly T[]; |
| selectedValue: T['value']; |
| selectedOptionPrefix?: string; |
| label?: string | undefined; |
| id?: string | undefined; |
| errorMessage?: string | undefined; |
| } |
| |
| let { |
| options, |
| selectedValue = $bindable(), |
| selectedOptionPrefix = '', |
| label = undefined, |
| id = undefined, |
| errorMessage = undefined |
| }: Props<GenericOption> = $props(); |
| |
| let listbox = $state( |
| createListbox({ |
| label: 'Actions', |
| selected: options.find((option) => option.value === selectedValue) ?? options[0] |
| }) |
| ); |
| |
| $effect(() => { |
| const newSelectedOption = options.find((option) => option.value === selectedValue); |
| if (newSelectedOption) { |
| listbox.set({ selected: newSelectedOption }); |
| } |
| }); |
| |
| const dispatch = createEventDispatcher<{ selectedValue: string }>(); |
| |
| function onSelect(e: Event) { |
| const selected = (e as CustomEvent).detail.selected.value; |
| selectedValue = selected; |
| dispatch('selectedValue', selected); |
| } |
| </script> |
| |
| <div class="flex flex-col gap-2"> |
| {#if label} |
| <label for={id} class="text-sm ml-1 text-color"> |
| {label} |
| </label> |
| {/if} |
| |
| <div class="relative w-full"> |
| <button |
| use:listbox.button |
| onselect={onSelect} |
| class={twMerge( |
| 'rounded-md dark:bg-shade-d400 w-full px-4 ring-1 ring-gray-300 dark:ring-gray-500 flex items-center h-[40px] text-color focus-within:ring-2 focus-within:ring-gray-400 transition group relative', |
| $listbox.expanded && 'ring-creator-gray4', |
| errorMessage && 'ring-red-600! ring-2' |
| )} |
| > |
| <span class="block truncate text-sm">{selectedOptionPrefix} {$listbox.selected.name}</span> |
| <Icon |
| name="chevronDown" |
| class={twMerge( |
| ' pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 w-[18px] transition-transform', |
| $listbox.expanded && '-rotate-180' |
| )} |
| /> |
| </button> |
| |
| {#if $listbox.expanded} |
| <ul |
| use:listbox.items |
| transition:fade={{ duration: 100 }} |
| class="absolute mt-1 max-h-60 py-1 z-20 w-full border overflow-auto dark:bg-shade-d400 bg-shade-l200 rounded-md text-sm shadow-lg text-color" |
| > |
| {#each options as option, idx (idx)} |
| {@const selected = $listbox.selected === option} |
| <li |
| class={twMerge( |
| 'relative cursor-default select-none py-2 pr-4 transition-colors p-[10px] hoverable', |
| selected && 'dark:!bg-shade-d170 !bg-shade-l600' |
| )} |
| use:listbox.item={{ value: option }} |
| > |
| <span class="block truncate {selected ? 'font-medium' : 'font-normal'}" |
| >{option.name}</span |
| > |
| </li> |
| {/each} |
| </ul> |
| {/if} |
| </div> |
| |
| {#if errorMessage} |
| <span class="text-red-600 text-xs font-medium ml-1"> |
| {errorMessage} |
| </span> |
| {/if} |
| </div> |