fix(channels): preserve model tags during IME composition

This commit is contained in:
chihao.ou
2026-09-22 21:45:48 +08:00
parent b0e32fe5be
commit a1eaad6033
2 changed files with 38 additions and 4 deletions
@@ -23,9 +23,9 @@
type="text"
class="flex-1 min-w-[120px] border-none bg-transparent text-sm outline-none placeholder:text-gray-400 dark:text-white"
:placeholder="models.length === 0 ? placeholder : ''"
@keydown.enter.prevent="addModel"
@keydown.enter="handleEnter"
@keydown.tab="handleTab"
@keydown.delete="handleBackspace"
@keydown.backspace="handleBackspace"
@paste="handlePaste"
@blur="addModel"
/>
@@ -66,8 +66,14 @@ function addModel() {
inputValue.value = ''
}
function handleEnter(event: KeyboardEvent) {
if (event.isComposing) return
event.preventDefault()
addModel()
}
function handleTab(event: KeyboardEvent) {
if (!inputValue.value.trim()) return
if (event.isComposing || !inputValue.value.trim()) return
event.preventDefault()
addModel()
}
@@ -78,7 +84,8 @@ function removeModel(idx: number) {
emit('update:models', newModels)
}
function handleBackspace() {
function handleBackspace(event: KeyboardEvent) {
if (event.isComposing) return
if (inputValue.value === '' && props.models.length > 0) {
removeModel(props.models.length - 1)
}
@@ -13,6 +13,33 @@ function pressTab(input: Element, shiftKey = false) {
}
describe('model tag keyboard navigation', () => {
it.each(['Enter', 'Tab', 'Backspace'])('leaves composing %s to the input method', async (key) => {
const wrapper = mount(ModelTagInput, { props: { models: ['existing'] } })
const input = wrapper.get('input')
if (key !== 'Backspace') await input.setValue('prefix')
const event = new KeyboardEvent('keydown', { key, isComposing: true, bubbles: true, cancelable: true })
input.element.dispatchEvent(event)
expect(event.defaultPrevented).toBe(false)
expect(wrapper.emitted('update:models')).toBeUndefined()
})
it('keeps tags on Delete and removes the last tag only on Backspace', async () => {
const wrapper = mount(ModelTagInput, { props: { models: ['existing'] } })
await wrapper.get('input').trigger('keydown', { key: 'Delete' })
expect(wrapper.emitted('update:models')).toBeUndefined()
await wrapper.get('input').trigger('keydown', { key: 'Backspace' })
expect(wrapper.emitted('update:models')).toEqual([[[]]])
})
it('commits ordinary Enter input and prevents form submission', async () => {
const wrapper = mount(ModelTagInput, { props: { models: [] } })
const input = wrapper.get('input'); await input.setValue('new-model')
const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })
input.element.dispatchEvent(event)
expect(event.defaultPrevented).toBe(true)
expect(wrapper.emitted('update:models')).toEqual([[['new-model']]])
})
it.each([false, true])('allows leaving an empty input with Tab (shift: %s)', (shift) => {
const wrapper = mount(ModelTagInput, { props: { models: ['gpt-4o'] } })
expect(pressTab(wrapper.get('input').element, shift).defaultPrevented).toBe(false)