diff --git a/src/components/AppSelect.vue b/src/components/AppSelect.vue index f19a951..ed94b08 100644 --- a/src/components/AppSelect.vue +++ b/src/components/AppSelect.vue @@ -10,6 +10,7 @@ interface Props { placeholder?: string disabled?: boolean placeholderValue?: any + searchable?: boolean } const props = withDefaults(defineProps(), { @@ -18,6 +19,7 @@ const props = withDefaults(defineProps(), { placeholder: 'Select...', disabled: false, placeholderValue: undefined, + searchable: false, }) const emit = defineEmits<{ @@ -29,6 +31,8 @@ const highlightedIndex = ref(-1) const triggerRef = ref(null) const panelRef = ref(null) const panelStyle = ref>({}) +const searchQuery = ref('') +const searchInputRef = ref(null) // Build the full list: placeholder + real options const allItems = computed(() => { @@ -40,6 +44,15 @@ const allItems = computed(() => { return [placeholderItem, ...props.options] }) +const filteredItems = computed(() => { + if (!props.searchable || !searchQuery.value) return allItems.value + const q = searchQuery.value.toLowerCase() + return allItems.value.filter(item => { + if (item._isPlaceholder) return true + return getOptionLabel(item).toLowerCase().includes(q) + }) +}) + const selectedLabel = computed(() => { const option = props.options.find( (o) => o[props.valueKey] === props.modelValue @@ -103,6 +116,11 @@ function open() { const selectedIdx = allItems.value.findIndex((item) => isSelected(item)) highlightedIndex.value = selectedIdx >= 0 ? selectedIdx : 0 + if (props.searchable) { + searchQuery.value = '' + nextTick(() => searchInputRef.value?.focus()) + } + nextTick(() => { scrollHighlightedIntoView() }) @@ -166,7 +184,7 @@ function onKeydown(e: KeyboardEvent) { e.preventDefault() highlightedIndex.value = Math.min( highlightedIndex.value + 1, - allItems.value.length - 1 + filteredItems.value.length - 1 ) nextTick(() => scrollHighlightedIntoView()) break @@ -179,7 +197,7 @@ function onKeydown(e: KeyboardEvent) { case ' ': e.preventDefault() if (highlightedIndex.value >= 0) { - select(allItems.value[highlightedIndex.value]) + select(filteredItems.value[highlightedIndex.value]) } break case 'Escape': @@ -193,6 +211,12 @@ function onKeydown(e: KeyboardEvent) { } } +function onSearchKeydown(e: KeyboardEvent) { + if (['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.key)) { + onKeydown(e) + } +} + onMounted(() => { // Nothing needed on mount since listeners are added when opened }) @@ -250,9 +274,19 @@ onBeforeUnmount(() => { :style="panelStyle" class="bg-bg-surface border border-border-visible rounded-xl shadow-[0_4px_24px_rgba(0,0,0,0.4)] overflow-hidden animate-dropdown-enter" > +
+ +