- Add disabled prop to combobox component to support disabled state - Update styling to include better focus states and border color
115 lines
3.3 KiB
Vue
115 lines
3.3 KiB
Vue
<script setup lang="ts">
|
|
import { cn } from '~/lib/utils'
|
|
|
|
interface Item {
|
|
value: string
|
|
label: string
|
|
code?: string
|
|
}
|
|
|
|
const props = defineProps<{
|
|
id: string
|
|
modelValue?: string
|
|
items: Item[]
|
|
placeholder?: string
|
|
searchPlaceholder?: string
|
|
emptyMessage?: string
|
|
class?: string
|
|
disabled?: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: string]
|
|
}>()
|
|
|
|
const open = ref(false)
|
|
|
|
const selectedItem = computed(() =>
|
|
props.items.find(item => item.value === props.modelValue),
|
|
)
|
|
|
|
const displayText = computed(() =>
|
|
selectedItem.value?.label || props.placeholder || '---pilih item',
|
|
)
|
|
|
|
// Create searchable items with combined code and label for better search
|
|
// Sort by:
|
|
// 1. Selected item first (highest priority)
|
|
// 2. Then by label alphabetically
|
|
const searchableItems = computed(() => {
|
|
const itemsWithSearch = props.items.map(item => ({
|
|
...item,
|
|
searchValue: `${item.code || ''} ${item.label}`.trim(),
|
|
isSelected: item.value === props.modelValue,
|
|
}))
|
|
|
|
return itemsWithSearch.sort((a, b) => {
|
|
// Selected item always comes first
|
|
if (a.isSelected && !b.isSelected) return -1
|
|
if (!a.isSelected && b.isSelected) return 1
|
|
|
|
// If neither or both are selected, sort by label alphabetically
|
|
return a.label.localeCompare(b.label)
|
|
})
|
|
})
|
|
|
|
function onSelect(item: Item) {
|
|
emit('update:modelValue', item.value)
|
|
open.value = false
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Popover v-model:open="open">
|
|
<PopoverTrigger as-child>
|
|
<Button
|
|
:id="props.id"
|
|
:disabled="props.disabled"
|
|
variant="outline"
|
|
role="combobox"
|
|
:aria-expanded="open"
|
|
:aria-controls="`${props.id}-list`"
|
|
:aria-describedby="`${props.id}-search`"
|
|
:class="cn(
|
|
'w-full justify-between border-1 border-gray-400 bg-white hover:bg-gray-50 text-sm font-normal focus:outline-none focus:ring-1 focus:ring-black',
|
|
!modelValue && 'text-muted-foreground',
|
|
props.class,
|
|
)"
|
|
>
|
|
{{ displayText }}
|
|
<Icon name="i-lucide-chevrons-up-down" class="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent class="w-full p-0">
|
|
<Command>
|
|
<CommandInput
|
|
:id="`${props.id}-search`"
|
|
class="h-9"
|
|
:placeholder="searchPlaceholder || 'Cari...'"
|
|
:aria-label="`Cari ${displayText}`"
|
|
/>
|
|
<CommandEmpty>{{ emptyMessage || 'Item tidak ditemukan.' }}</CommandEmpty>
|
|
<CommandList :id="`${props.id}-list`" role="listbox">
|
|
<CommandGroup>
|
|
<CommandItem v-for="item in searchableItems" :key="item.value" :value="item.searchValue" @select="onSelect(item)">
|
|
<div class="flex items-center justify-between w-full">
|
|
<span>{{ item.label }}</span>
|
|
<div class="flex items-center gap-2">
|
|
<span v-if="item.code" class="text-xs text-muted-foreground">{{ item.code }}</span>
|
|
<Icon
|
|
name="i-lucide-check"
|
|
:class="cn(
|
|
'h-4 w-4',
|
|
modelValue === item.value ? 'opacity-100' : 'opacity-0',
|
|
)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</template>
|