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