feat(ui): 增强Select组件功能和样式
This commit is contained in:
@@ -67,12 +67,13 @@
|
||||
:aria-selected="isSelected(option)"
|
||||
:aria-disabled="isOptionDisabled(option)"
|
||||
@click.stop="!isOptionDisabled(option) && selectOption(option)"
|
||||
@mouseenter="focusedIndex = index"
|
||||
@mouseenter="handleOptionMouseEnter(option, index)"
|
||||
:class="[
|
||||
'select-option',
|
||||
isGroupHeaderOption(option) && 'select-option-group',
|
||||
isSelected(option) && 'select-option-selected',
|
||||
isOptionDisabled(option) && 'select-option-disabled',
|
||||
focusedIndex === index && 'select-option-focused'
|
||||
isOptionDisabled(option) && !isGroupHeaderOption(option) && 'select-option-disabled',
|
||||
focusedIndex === index && !isGroupHeaderOption(option) && 'select-option-focused'
|
||||
]"
|
||||
>
|
||||
<slot name="option" :option="option" :selected="isSelected(option)">
|
||||
@@ -201,6 +202,13 @@ const isOptionDisabled = (option: any): boolean => {
|
||||
return false
|
||||
}
|
||||
|
||||
const isGroupHeaderOption = (option: any): boolean => {
|
||||
if (typeof option === 'object' && option !== null) {
|
||||
return option.kind === 'group'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const selectedOption = computed(() => {
|
||||
return props.options.find((opt) => getOptionValue(opt) === props.modelValue) || null
|
||||
})
|
||||
@@ -225,6 +233,31 @@ const isSelected = (option: any): boolean => {
|
||||
return getOptionValue(option) === props.modelValue
|
||||
}
|
||||
|
||||
const findNextEnabledIndex = (startIndex: number): number => {
|
||||
const opts = filteredOptions.value
|
||||
if (opts.length === 0) return -1
|
||||
for (let offset = 0; offset < opts.length; offset++) {
|
||||
const idx = (startIndex + offset) % opts.length
|
||||
if (!isOptionDisabled(opts[idx])) return idx
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
const findPrevEnabledIndex = (startIndex: number): number => {
|
||||
const opts = filteredOptions.value
|
||||
if (opts.length === 0) return -1
|
||||
for (let offset = 0; offset < opts.length; offset++) {
|
||||
const idx = (startIndex - offset + opts.length) % opts.length
|
||||
if (!isOptionDisabled(opts[idx])) return idx
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
const handleOptionMouseEnter = (option: any, index: number) => {
|
||||
if (isOptionDisabled(option) || isGroupHeaderOption(option)) return
|
||||
focusedIndex.value = index
|
||||
}
|
||||
|
||||
// Update trigger rect periodically while open to follow scroll/resize
|
||||
const updateTriggerRect = () => {
|
||||
if (containerRef.value) {
|
||||
@@ -259,8 +292,15 @@ watch(isOpen, (open) => {
|
||||
if (open) {
|
||||
calculateDropdownPosition()
|
||||
// Reset focused index to current selection or first item
|
||||
const selectedIdx = filteredOptions.value.findIndex(isSelected)
|
||||
focusedIndex.value = selectedIdx >= 0 ? selectedIdx : 0
|
||||
if (filteredOptions.value.length === 0) {
|
||||
focusedIndex.value = -1
|
||||
} else {
|
||||
const selectedIdx = filteredOptions.value.findIndex(isSelected)
|
||||
const initialIdx = selectedIdx >= 0 ? selectedIdx : 0
|
||||
focusedIndex.value = isOptionDisabled(filteredOptions.value[initialIdx])
|
||||
? findNextEnabledIndex(initialIdx + 1)
|
||||
: initialIdx
|
||||
}
|
||||
|
||||
if (props.searchable) {
|
||||
nextTick(() => searchInputRef.value?.focus())
|
||||
@@ -295,13 +335,13 @@ const onDropdownKeyDown = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
focusedIndex.value = (focusedIndex.value + 1) % filteredOptions.value.length
|
||||
scrollToFocused()
|
||||
focusedIndex.value = findNextEnabledIndex(focusedIndex.value + 1)
|
||||
if (focusedIndex.value >= 0) scrollToFocused()
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
focusedIndex.value = (focusedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length
|
||||
scrollToFocused()
|
||||
focusedIndex.value = findPrevEnabledIndex(focusedIndex.value - 1)
|
||||
if (focusedIndex.value >= 0) scrollToFocused()
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
@@ -441,6 +481,17 @@ onUnmounted(() => {
|
||||
@apply cursor-not-allowed opacity-40;
|
||||
}
|
||||
|
||||
.select-dropdown-portal .select-option-group {
|
||||
@apply cursor-default select-none;
|
||||
@apply bg-gray-50 dark:bg-dark-900;
|
||||
@apply text-[11px] font-bold uppercase tracking-wider;
|
||||
@apply text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.select-dropdown-portal .select-option-group:hover {
|
||||
@apply bg-gray-50 dark:bg-dark-900;
|
||||
}
|
||||
|
||||
.select-dropdown-portal .select-option-label {
|
||||
@apply flex-1 min-w-0 truncate text-left;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user