mirror of
https://github.com/element-plus/element-plus.git
synced 2025-02-17 11:49:41 +08:00
fix(components): [input] lose focus when click suffix icon (#13264)
* feat(hooks): add useFocusController * fix(components): [input] lose focus when click suffix icon closed #13153, #13159 * refactor(hooks): use-focus-controller * test: fix test * test: update * fix(theme-chalk): [input] add cursor style to the wrapper
This commit is contained in:
parent
a1dd70eead
commit
704399fadd
@ -15,10 +15,10 @@
|
||||
<slot name="prepend" />
|
||||
</div>
|
||||
|
||||
<div :class="wrapperKls">
|
||||
<div ref="wrapperRef" :class="wrapperKls">
|
||||
<!-- prefix slot -->
|
||||
<span v-if="$slots.prefix || prefixIcon" :class="nsInput.e('prefix')">
|
||||
<span :class="nsInput.e('prefix-inner')" @click="focus">
|
||||
<span :class="nsInput.e('prefix-inner')">
|
||||
<slot name="prefix" />
|
||||
<el-icon v-if="prefixIcon" :class="nsInput.e('icon')">
|
||||
<component :is="prefixIcon" />
|
||||
@ -54,7 +54,7 @@
|
||||
|
||||
<!-- suffix slot -->
|
||||
<span v-if="suffixVisible" :class="nsInput.e('suffix')">
|
||||
<span :class="nsInput.e('suffix-inner')" @click="focus">
|
||||
<span :class="nsInput.e('suffix-inner')">
|
||||
<template
|
||||
v-if="!showClear || !showPwdVisible || !isWordLimitVisible"
|
||||
>
|
||||
@ -172,7 +172,12 @@ import {
|
||||
isKorean,
|
||||
isObject,
|
||||
} from '@element-plus/utils'
|
||||
import { useAttrs, useCursor, useNamespace } from '@element-plus/hooks'
|
||||
import {
|
||||
useAttrs,
|
||||
useCursor,
|
||||
useFocusController,
|
||||
useNamespace,
|
||||
} from '@element-plus/hooks'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { calcTextareaHeight } from './utils'
|
||||
import { inputEmits, inputProps } from './input'
|
||||
@ -220,7 +225,7 @@ const containerKls = computed(() => [
|
||||
|
||||
const wrapperKls = computed(() => [
|
||||
nsInput.e('wrapper'),
|
||||
nsInput.is('focus', focused.value),
|
||||
nsInput.is('focus', isFocused.value),
|
||||
])
|
||||
|
||||
const attrs = useAttrs({
|
||||
@ -240,7 +245,6 @@ const nsTextarea = useNamespace('textarea')
|
||||
const input = shallowRef<HTMLInputElement>()
|
||||
const textarea = shallowRef<HTMLTextAreaElement>()
|
||||
|
||||
const focused = ref(false)
|
||||
const hovering = ref(false)
|
||||
const isComposing = ref(false)
|
||||
const passwordVisible = ref(false)
|
||||
@ -249,6 +253,17 @@ const textareaCalcStyle = shallowRef(props.inputStyle)
|
||||
|
||||
const _ref = computed(() => input.value || textarea.value)
|
||||
|
||||
const { wrapperRef, isFocused, handleFocus, handleBlur } = useFocusController(
|
||||
_ref,
|
||||
{
|
||||
afterBlur() {
|
||||
if (props.validateEvent) {
|
||||
formItem?.validate?.('blur').catch((err) => debugWarn(err))
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const needStatusIcon = computed(() => form?.statusIcon ?? false)
|
||||
const validateState = computed(() => formItem?.validateState || '')
|
||||
const validateIcon = computed(
|
||||
@ -275,7 +290,7 @@ const showClear = computed(
|
||||
!inputDisabled.value &&
|
||||
!props.readonly &&
|
||||
!!nativeInputValue.value &&
|
||||
(focused.value || hovering.value)
|
||||
(isFocused.value || hovering.value)
|
||||
)
|
||||
const showPwdVisible = computed(
|
||||
() =>
|
||||
@ -283,7 +298,7 @@ const showPwdVisible = computed(
|
||||
!inputDisabled.value &&
|
||||
!props.readonly &&
|
||||
!!nativeInputValue.value &&
|
||||
(!!nativeInputValue.value || focused.value)
|
||||
(!!nativeInputValue.value || isFocused.value)
|
||||
)
|
||||
const isWordLimitVisible = computed(
|
||||
() =>
|
||||
@ -445,19 +460,6 @@ const focus = async () => {
|
||||
|
||||
const blur = () => _ref.value?.blur()
|
||||
|
||||
const handleFocus = (event: FocusEvent) => {
|
||||
focused.value = true
|
||||
emit('focus', event)
|
||||
}
|
||||
|
||||
const handleBlur = (event: FocusEvent) => {
|
||||
focused.value = false
|
||||
emit('blur', event)
|
||||
if (props.validateEvent) {
|
||||
formItem?.validate?.('blur').catch((err) => debugWarn(err))
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseLeave = (evt: MouseEvent) => {
|
||||
hovering.value = false
|
||||
emit('mouseleave', evt)
|
||||
|
55
packages/hooks/__tests__/use-focus-controller.test.tsx
Normal file
55
packages/hooks/__tests__/use-focus-controller.test.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useFocusController } from '../use-focus-controller'
|
||||
|
||||
describe('useFocusController', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('it will avoid trigger unnecessary blur event', async () => {
|
||||
const focusHandler = vi.fn()
|
||||
const blurHandler = vi.fn()
|
||||
const wrapper = mount({
|
||||
emits: ['focus', 'blur'],
|
||||
setup() {
|
||||
const targetRef = ref()
|
||||
const { wrapperRef, isFocused, handleFocus, handleBlur } =
|
||||
useFocusController(targetRef, {
|
||||
afterFocus: focusHandler,
|
||||
afterBlur: blurHandler,
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div ref={wrapperRef}>
|
||||
<input ref={targetRef} onFocus={handleFocus} onBlur={handleBlur} />
|
||||
<span>{String(isFocused.value)}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
expect(wrapper.find('span').text()).toBe('false')
|
||||
expect(wrapper.find('div').attributes('tabindex')).toBe('-1')
|
||||
expect(focusHandler).toHaveBeenCalledTimes(0)
|
||||
expect(blurHandler).toHaveBeenCalledTimes(0)
|
||||
|
||||
await wrapper.find('input').trigger('focus')
|
||||
expect(wrapper.emitted()).toHaveProperty('focus')
|
||||
expect(wrapper.find('span').text()).toBe('true')
|
||||
expect(focusHandler).toHaveBeenCalledTimes(1)
|
||||
expect(blurHandler).toHaveBeenCalledTimes(0)
|
||||
|
||||
await wrapper.find('span').trigger('click')
|
||||
expect(wrapper.emitted()).not.toHaveProperty('blur')
|
||||
expect(focusHandler).toHaveBeenCalledTimes(1)
|
||||
expect(blurHandler).toHaveBeenCalledTimes(0)
|
||||
|
||||
await wrapper.find('input').trigger('blur')
|
||||
expect(wrapper.emitted()).toHaveProperty('blur')
|
||||
expect(wrapper.find('span').text()).toBe('false')
|
||||
expect(blurHandler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
@ -26,3 +26,4 @@ export * from './use-floating'
|
||||
export * from './use-cursor'
|
||||
export * from './use-ordered-children'
|
||||
export * from './use-size'
|
||||
export * from './use-focus-controller'
|
||||
|
60
packages/hooks/use-focus-controller/index.ts
Normal file
60
packages/hooks/use-focus-controller/index.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { getCurrentInstance, ref, shallowRef, watch } from 'vue'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import type { ShallowRef } from 'vue'
|
||||
|
||||
interface UseFocusControllerOptions {
|
||||
afterFocus?: () => void
|
||||
afterBlur?: () => void
|
||||
}
|
||||
|
||||
export function useFocusController<T extends HTMLElement>(
|
||||
target: ShallowRef<T | undefined>,
|
||||
{ afterFocus, afterBlur }: UseFocusControllerOptions = {}
|
||||
) {
|
||||
const instance = getCurrentInstance()!
|
||||
const { emit } = instance
|
||||
const wrapperRef = shallowRef<HTMLElement>()
|
||||
const isFocused = ref(false)
|
||||
|
||||
const handleFocus = (event: FocusEvent) => {
|
||||
if (isFocused.value) return
|
||||
isFocused.value = true
|
||||
emit('focus', event)
|
||||
afterFocus?.()
|
||||
}
|
||||
|
||||
const handleBlur = (event: FocusEvent) => {
|
||||
if (
|
||||
event.relatedTarget &&
|
||||
wrapperRef.value?.contains(event.relatedTarget as Node)
|
||||
)
|
||||
return
|
||||
|
||||
isFocused.value = false
|
||||
emit('blur', event)
|
||||
afterBlur?.()
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
target.value?.focus()
|
||||
}
|
||||
|
||||
watch(wrapperRef, (el) => {
|
||||
if (el) {
|
||||
el.setAttribute('role', 'button')
|
||||
el.setAttribute('tabindex', '-1')
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: using useEventListener will fail the test
|
||||
// useEventListener(target, 'focus', handleFocus)
|
||||
// useEventListener(target, 'blur', handleBlur)
|
||||
useEventListener(wrapperRef, 'click', handleClick)
|
||||
|
||||
return {
|
||||
wrapperRef,
|
||||
isFocused,
|
||||
handleFocus,
|
||||
handleBlur,
|
||||
}
|
||||
}
|
@ -180,6 +180,7 @@
|
||||
'input-border-radius',
|
||||
map.get($input, 'border-radius')
|
||||
);
|
||||
cursor: text;
|
||||
transition: getCssVar('transition-box-shadow');
|
||||
transform: translate3d(0, 0, 0);
|
||||
@include inset-input-border(
|
||||
|
Loading…
Reference in New Issue
Block a user