Select.vue
1 <template> 2 <div 3 class="select-container" 4 @click="toggleDropdown" 5 @blur="closeDropdown" 6 tabindex="0"> 7 <div class="dropdown-menu" v-show="isOpen"> 8 <div 9 v-for="(item, index) in items" 10 :key="index" 11 class="dropdown-item" 12 :class="{ selected: selectedItem && selectedItem.value === item.value }" 13 @click.stop="selectItem(item)"> 14 {{ item.label }} 15 <div class="key-container" v-if="item.key"> 16 <template v-if="item.key.includes('+')"> 17 <span v-for="(part, index) in item.key.split('+')" :key="index"> 18 <UiKey :keyName="part" /> 19 </span> 20 </template> 21 <template v-else> 22 <UiKey :keyName="item.key" /> 23 </template> 24 </div> 25 </div> 26 </div> 27 <div class="selected-item"> 28 {{ selectedItem ? selectedItem.label : placeholder }} 29 <LucideChevronsDownUp :size="16" v-if="isOpen" /> 30 <LucideChevronsUpDown :size="16" v-else /> 31 </div> 32 </div> 33 </template> 34 35 <script setup lang="ts" generic="T"> 36 import { ref, computed } from "vue"; 37 import { LucideChevronsDownUp, LucideChevronsUpDown } from "lucide-vue-next"; 38 39 interface SelectItem<V> { 40 label: string; 41 value: V; 42 key?: string; 43 } 44 45 const props = defineProps<{ 46 items: SelectItem<T>[]; 47 modelValue?: T; 48 placeholder?: string; 49 }>(); 50 51 const emit = defineEmits<{ 52 "update:modelValue": [value: T]; 53 }>(); 54 55 const isOpen = ref(false); 56 57 const selectedItem = computed<SelectItem<T> | null>(() => { 58 if (props.modelValue !== undefined) { 59 return props.items.find((item) => item.value === props.modelValue) ?? null; 60 } 61 return null; 62 }); 63 64 const toggleDropdown = () => { 65 isOpen.value = !isOpen.value; 66 }; 67 68 const closeDropdown = () => { 69 isOpen.value = false; 70 }; 71 72 const selectItem = (item: SelectItem<T>) => { 73 emit("update:modelValue", item.value); 74 isOpen.value = false; 75 }; 76 </script> 77 78 <style scoped> 79 .select-container { 80 position: relative; 81 cursor: pointer; 82 user-select: none; 83 outline: none; 84 display: flex; 85 flex-direction: column; 86 gap: 8px; 87 } 88 89 .selected-item { 90 display: flex; 91 align-items: center; 92 gap: 8px; 93 } 94 95 .dropdown-menu { 96 position: absolute; 97 bottom: 100%; 98 right: 0; 99 background-color: var(--background); 100 z-index: 10; 101 display: flex; 102 flex-direction: column; 103 margin-bottom: 8px; 104 margin-right: -8px; 105 border: 1px solid var(--border); 106 } 107 108 .dropdown-item { 109 display: flex; 110 justify-content: space-between; 111 align-items: center; 112 width: 146px; 113 padding: 8px; 114 } 115 116 .dropdown-item:hover { 117 background-color: var(--element); 118 } 119 120 .key-container { 121 display: flex; 122 align-items: center; 123 gap: 2px; 124 } 125 </style>