fix(components): [select] abnormal focus when click tags (#13699)

* fix(components): [select] abnormal focus when click tags

closed #13665

* fix(components): [select] clearable

* chore(components): [select] remove console

* fix: the setTimeout function may bring some side effects

* fix: remove role

* test(components): [select] add some test
This commit is contained in:
qiang 2023-08-03 20:15:57 +08:00 committed by GitHub
parent 3ba7babc74
commit 0109ab6195
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 167 additions and 31 deletions

View File

@ -7,7 +7,6 @@ import { ArrowDown, CaretTop, CircleClose } from '@element-plus/icons-vue'
import { usePopperContainerId } from '@element-plus/hooks'
import { hasClass } from '@element-plus/utils'
import { ElFormItem } from '@element-plus/components/form'
import sleep from '@element-plus/test-utils/sleep'
import Select from '../src/select.vue'
import Group from '../src/option-group.vue'
import Option from '../src/option.vue'
@ -1222,10 +1221,72 @@ describe('Select', () => {
expect(input.exists()).toBe(true)
await input.trigger('focus')
expect(handleFocus).toHaveBeenCalled()
expect(handleFocus).toHaveBeenCalledTimes(1)
await input.trigger('blur')
await sleep(0)
expect(handleBlur).toHaveBeenCalled()
expect(handleBlur).toHaveBeenCalledTimes(1)
await input.trigger('focus')
expect(handleFocus).toHaveBeenCalledTimes(2)
await input.trigger('blur')
expect(handleBlur).toHaveBeenCalledTimes(2)
})
test('event:focus & blur for clearable & filterable', async () => {
const handleFocus = vi.fn()
const handleBlur = vi.fn()
wrapper = _mount(
`<el-select
v-model="value"
clearable
filterable
@focus="handleFocus"
@blur="handleBlur"
>
<el-option
v-for="item in options"
:label="item.label"
:key="item.value"
:value="item.value"
/>
</el-select>`,
() => ({
options: [
{
value: '选项1',
label: '黄金糕',
},
],
value: '选项1',
handleFocus,
handleBlur,
})
)
const select = wrapper.findComponent({ name: 'ElSelect' })
const vm = wrapper.vm as any
const selectVm = select.vm as any
selectVm.inputHovering = true
await selectVm.$nextTick()
const iconClear = wrapper.findComponent(CircleClose)
expect(iconClear.exists()).toBe(true)
await iconClear.trigger('click')
expect(vm.value).toBe('')
expect(handleFocus).toHaveBeenCalledTimes(1)
expect(handleBlur).not.toHaveBeenCalled()
const options = getOptions()
options[0].click()
await nextTick()
expect(vm.value).toBe('选项1')
selectVm.inputHovering = true
await iconClear.trigger('click')
expect(handleFocus).toHaveBeenCalledTimes(1)
expect(handleBlur).not.toHaveBeenCalled()
const input = select.find('input')
await input.trigger('blur')
expect(handleBlur).toHaveBeenCalledTimes(1)
})
test('event:focus & blur for multiple & filterable select', async () => {
@ -1251,8 +1312,73 @@ describe('Select', () => {
await input.trigger('focus')
expect(handleFocus).toHaveBeenCalled()
await input.trigger('blur')
await sleep(0)
expect(handleBlur).toHaveBeenCalled()
await input.trigger('focus')
expect(handleFocus).toHaveBeenCalledTimes(2)
await input.trigger('blur')
expect(handleBlur).toHaveBeenCalledTimes(2)
})
test('event:focus & blur for multiple tag close', async () => {
const handleFocus = vi.fn()
const handleBlur = vi.fn()
wrapper = _mount(
`<el-select
v-model="value"
multiple
@focus="handleFocus"
@blur="handleBlur"
>
<el-option
v-for="item in options"
:label="item.label"
:key="item.value"
:value="item.value">
<p>{{item.label}} {{item.value}}</p>
</el-option>
</el-select>`,
() => ({
options: [
{
value: '选项1',
label: '黄金糕',
},
{
value: '选项2',
label: '双皮奶',
},
{
value: '选项3',
label: '蚵仔煎',
},
{
value: '选项4',
label: '龙须面',
},
{
value: '选项5',
label: '北京烤鸭',
},
],
value: ['选项1', '选项2'],
handleFocus,
handleBlur,
})
)
const select = wrapper.findComponent({ name: 'ElSelect' })
const input = select.find('input')
await input.trigger('focus')
expect(handleFocus).toHaveBeenCalledTimes(1)
const tagCloseIcons = wrapper.findAll('.el-tag__close')
await tagCloseIcons[1].trigger('click')
await tagCloseIcons[0].trigger('click')
expect(handleFocus).toHaveBeenCalledTimes(1)
expect(handleBlur).not.toHaveBeenCalled()
await input.trigger('blur')
expect(handleBlur).toHaveBeenCalledTimes(1)
})
test('should not open popper when automatic-dropdown not set', async () => {

View File

@ -33,8 +33,10 @@
<div
v-if="multiple"
ref="tags"
tabindex="-1"
:class="tagsKls"
:style="selectTagsStyle"
@click="focus"
>
<transition
v-if="collapseTags && selected.length"
@ -279,7 +281,7 @@ import {
import { useResizeObserver } from '@vueuse/core'
import { placements } from '@popperjs/core'
import { ClickOutside } from '@element-plus/directives'
import { useFocus, useLocale, useNamespace } from '@element-plus/hooks'
import { useLocale, useNamespace } from '@element-plus/hooks'
import ElInput from '@element-plus/components/input'
import ElTooltip, {
useTooltipContentProps,
@ -462,6 +464,7 @@ export default defineComponent({
onOptionDestroy,
handleMenuEnter,
handleFocus,
focus,
blur,
handleBlur,
handleClearClick,
@ -490,8 +493,6 @@ export default defineComponent({
collapseTagList,
} = useSelect(props, states, ctx)
const { focus } = useFocus(reference)
const {
inputWidth,
selected,
@ -682,6 +683,7 @@ export default defineComponent({
handleComposition,
handleMenuEnter,
handleFocus,
focus,
blur,
handleBlur,
handleClearClick,
@ -692,7 +694,6 @@ export default defineComponent({
getValueKey,
navigateOptions,
dropMenuVisible,
focus,
reference,
input,

View File

@ -57,9 +57,9 @@ export function useSelectStates(props) {
isOnComposition: false,
prefixWidth: 11,
mouseEnter: false,
focused: false,
})
}
let ignoreFocusEvent = false
type States = ReturnType<typeof useSelectStates>
@ -270,7 +270,6 @@ export const useSelect = (props, states: States, ctx) => {
props.remoteMethod('')
}
}
input.value && input.value.blur()
states.query = ''
states.previousQuery = null
states.selectedLabel = ''
@ -637,6 +636,7 @@ export const useSelect = (props, states: States, ctx) => {
ctx.emit('remove-tag', tag.value)
}
event.stopPropagation()
focus()
}
const deleteSelected = (event) => {
@ -652,6 +652,7 @@ export const useSelect = (props, states: States, ctx) => {
states.hoverIndex = -1
states.visible = false
ctx.emit('clear')
focus()
}
const handleOptionSelect = (option) => {
@ -784,16 +785,23 @@ export const useSelect = (props, states: States, ctx) => {
}
const handleFocus = (event: FocusEvent) => {
if (!ignoreFocusEvent) {
if (!states.focused) {
if (props.automaticDropdown || props.filterable) {
if (props.filterable && !states.visible) {
states.menuVisibleOnFocus = true
}
states.visible = true
}
states.focused = true
ctx.emit('focus', event)
}
}
const focus = () => {
if (states.visible) {
;(input.value || reference.value)?.focus()
} else {
ignoreFocusEvent = false
reference.value?.focus()
}
}
@ -804,19 +812,19 @@ export const useSelect = (props, states: States, ctx) => {
}
const handleBlur = (event: FocusEvent) => {
setTimeout(() => {
// validate current focus event is inside el-tooltip-content or el-select
// if so, ignore the blur event and the next focus event
if (
tooltipRef.value?.isFocusInsideContent() ||
selectWrapper.value?.contains(event.relatedTarget)
) {
ignoreFocusEvent = true
return
}
states.visible && handleClose()
ctx.emit('blur', event)
})
// validate current focus event is inside el-tooltip-content or el-select
// if so, ignore the blur event.
if (
tooltipRef.value?.isFocusInsideContent(event) ||
tagTooltipRef.value?.isFocusInsideContent(event) ||
selectWrapper.value?.contains(event.relatedTarget)
) {
return
}
states.visible && handleClose()
states.focused = false
ctx.emit('blur', event)
}
const handleClearClick = (event: Event) => {
@ -847,9 +855,7 @@ export const useSelect = (props, states: States, ctx) => {
states.visible = !states.visible
}
}
if (states.visible) {
;(input.value || reference.value)?.focus()
}
focus()
}
}
@ -954,6 +960,7 @@ export const useSelect = (props, states: States, ctx) => {
onOptionDestroy,
handleMenuEnter,
handleFocus,
focus,
blur,
handleBlur,
handleClearClick,

View File

@ -154,10 +154,12 @@ watch(
}
)
const isFocusInsideContent = () => {
const isFocusInsideContent = (event?: FocusEvent) => {
const popperContent: HTMLElement | undefined =
contentRef.value?.contentRef?.popperContentRef
return popperContent && popperContent.contains(document.activeElement)
const activeElement = (event?.relatedTarget as Node) || document.activeElement
return popperContent && popperContent.contains(activeElement)
}
onDeactivated(() => open.value && hide())