fix(ui): focus non-searchable select dropdowns

This commit is contained in:
chihao.ou
2026-09-13 17:16:26 +08:00
parent bdb42e22f8
commit a985de062c
2 changed files with 58 additions and 0 deletions
@@ -55,6 +55,7 @@
:class="[instanceId]"
:style="dropdownStyle"
role="listbox"
tabindex="-1"
@click.stop
@mousedown.stop
@keydown="onDropdownKeyDown"
@@ -384,6 +385,8 @@ watch(isOpen, (open) => {
if (isSearchable.value) {
nextTick(() => searchInputRef.value?.focus())
} else {
nextTick(() => dropdownRef.value?.focus())
}
// Add scroll listener to update position
window.addEventListener('scroll', updateTriggerRect, { capture: true, passive: true })
@@ -0,0 +1,55 @@
import { mount } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import Select from '../Select.vue'
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) }))
let wrapper: ReturnType<typeof mount>
afterEach(() => { wrapper?.unmount(); document.body.innerHTML = '' })
async function press(key: string) {
document.activeElement!.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }))
await nextTick()
}
async function open(searchable: boolean | 'auto' = 'auto') {
wrapper = mount(Select, {
attachTo: document.body,
props: { modelValue: 'alpha', searchable, options: [
{ value: 'alpha', label: 'Alpha' },
{ value: 'disabled', label: 'Disabled', disabled: true },
{ value: 'beta', label: 'Beta' },
] },
})
wrapper.get<HTMLButtonElement>('button').element.focus()
await press('ArrowDown')
await nextTick()
}
describe('Select keyboard focus', () => {
it.each([false, 'auto'] as const)('selects an enabled option without a search input (%s)', async (searchable) => {
await open(searchable)
expect(document.activeElement).toBe(document.querySelector('[role="listbox"]'))
await press('ArrowDown')
await press('Enter')
expect(wrapper.emitted('update:modelValue')).toEqual([['beta']])
expect(wrapper.get('button').attributes('aria-expanded')).toBe('false')
expect(document.activeElement).toBe(wrapper.get('button').element)
})
it('allows Escape to close a non-searchable dropdown and restore trigger focus', async () => {
await open(false)
await press('Escape')
expect(wrapper.get('button').attributes('aria-expanded')).toBe('false')
expect(document.activeElement).toBe(wrapper.get('button').element)
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
})
it('keeps focus in the search input when search is enabled', async () => {
await open(true)
expect(document.activeElement).toBe(document.querySelector('.select-search-input'))
await press('ArrowDown')
await press('Enter')
expect(wrapper.emitted('update:modelValue')).toEqual([['beta']])
})
})