refactor(hooks): [useFocusController] add disabled attribute (#21032)

refactor(hooks): [useFocusController] add disabled prop

Co-authored-by: warmthsea <2586244885@qq.com>
This commit is contained in:
qiang
2025-07-01 16:41:35 +08:00
committed by GitHub
parent c5d37fbb0a
commit b881ef25cb
10 changed files with 80 additions and 51 deletions

View File

@@ -180,9 +180,7 @@ const triggerRef = ref()
const inputRef = ref()
const { isFocused, handleFocus, handleBlur } = useFocusController(triggerRef, {
beforeFocus() {
return colorDisabled.value
},
disabled: colorDisabled,
beforeBlur(event) {
return popper.value?.isFocusInsideContent(event)
},

View File

@@ -138,9 +138,7 @@ export function useInputTag({ props, emit, formItem }: UseInputTagOptions) {
}
const { wrapperRef, isFocused } = useFocusController(inputRef, {
beforeFocus() {
return disabled.value
},
disabled,
afterBlur() {
if (props.saveOnBlur) {
handleAddTag()

View File

@@ -256,9 +256,7 @@ const _ref = computed(() => input.value || textarea.value)
const { wrapperRef, isFocused, handleFocus, handleBlur } = useFocusController(
_ref,
{
beforeFocus() {
return inputDisabled.value
},
disabled: inputDisabled,
afterBlur() {
if (props.validateEvent) {
elFormItem?.validate?.('blur').catch((err) => debugWarn(err))

View File

@@ -192,9 +192,7 @@ const handleInputKeyDown = (event: KeyboardEvent | Event) => {
}
const { wrapperRef } = useFocusController(elInputRef, {
beforeFocus() {
return disabled.value
},
disabled,
afterFocus() {
syncAfterCursorMove()
},

View File

@@ -102,10 +102,10 @@ const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => {
afterComposition: (e) => onInput(e),
})
const selectDisabled = computed(() => props.disabled || !!elForm?.disabled)
const { wrapperRef, isFocused, handleBlur } = useFocusController(inputRef, {
beforeFocus() {
return selectDisabled.value
},
disabled: selectDisabled,
afterFocus() {
if (props.automaticDropdown && !expanded.value) {
expanded.value = true
@@ -138,8 +138,6 @@ const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => {
// the controller of the expanded popup
const expanded = ref(false)
const selectDisabled = computed(() => props.disabled || elForm?.disabled)
const needStatusIcon = computed(() => elForm?.statusIcon ?? false)
const popupHeight = computed(() => {

View File

@@ -92,6 +92,15 @@ export const useSelect = (props: SelectProps, emit: SelectEmits) => {
const tagMenuRef = ref<HTMLElement>()
const collapseItemRef = ref<HTMLElement>()
const scrollbarRef = ref<ScrollbarInstance>()
// the controller of the expanded popup
const expanded = ref(false)
const hoverOption = ref()
const { form, formItem } = useFormItem()
const { inputId } = useFormItemInputId(props, {
formItemContext: formItem,
})
const { valueOnClear, isEmptyValue } = useEmptyValues(props)
const {
isComposing,
@@ -102,10 +111,10 @@ export const useSelect = (props: SelectProps, emit: SelectEmits) => {
afterComposition: (e) => onInput(e),
})
const selectDisabled = computed(() => props.disabled || !!form?.disabled)
const { wrapperRef, isFocused, handleBlur } = useFocusController(inputRef, {
beforeFocus() {
return selectDisabled.value
},
disabled: selectDisabled,
afterFocus() {
if (props.automaticDropdown && !expanded.value) {
expanded.value = true
@@ -127,18 +136,6 @@ export const useSelect = (props: SelectProps, emit: SelectEmits) => {
},
})
// the controller of the expanded popup
const expanded = ref(false)
const hoverOption = ref()
const { form, formItem } = useFormItem()
const { inputId } = useFormItemInputId(props, {
formItemContext: formItem,
})
const { valueOnClear, isEmptyValue } = useEmptyValues(props)
const selectDisabled = computed(() => props.disabled || form?.disabled)
const hasModelValue = computed(() => {
return isArray(props.modelValue)
? props.modelValue.length > 0

View File

@@ -39,7 +39,7 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { useAttrs, useFocusController, useNamespace } from '@element-plus/hooks'
import { timePickerRangeTriggerProps } from './props'
@@ -50,7 +50,7 @@ defineOptions({
inheritAttrs: false,
})
defineProps(timePickerRangeTriggerProps)
const props = defineProps(timePickerRangeTriggerProps)
const emit = defineEmits([
'mouseenter',
'mouseleave',
@@ -71,7 +71,9 @@ const nsRange = useNamespace('range')
const inputRef = ref<HTMLInputElement>()
const endInputRef = ref<HTMLInputElement>()
const { wrapperRef, isFocused } = useFocusController(inputRef)
const { wrapperRef, isFocused } = useFocusController(inputRef, {
disabled: computed(() => props.disabled),
})
const handleClick = (evt: MouseEvent) => {
emit('click', evt)

View File

@@ -246,9 +246,14 @@ const pickerActualVisible = ref(false)
const valueOnOpen = ref<TimePickerDefaultProps['modelValue'] | null>(null)
let hasJustTabExitedInput = false
const pickerDisabled = computed(() => {
return props.disabled || !!form?.disabled
})
const { isFocused, handleFocus, handleBlur } = useFocusController(inputRef, {
disabled: pickerDisabled,
beforeFocus() {
return props.readonly || pickerDisabled.value
return props.readonly
},
afterFocus() {
pickerVisible.value = true
@@ -385,10 +390,6 @@ const handleClose = () => {
pickerVisible.value = false
}
const pickerDisabled = computed(() => {
return props.disabled || form?.disabled
})
const parsedValue = computed(() => {
let dayOrDays: DayOrDays
if (valueIsEmpty.value) {

View File

@@ -225,4 +225,34 @@ describe('useFocusController', () => {
expect(wrapper.find('span').text()).toBe('false')
expect(blurHandler).toHaveBeenCalled()
})
it('It will decide whether to trigger focus and blur based on the disabled state', async () => {
const disabled = ref(true)
const wrapper = mount({
emits: ['focus', 'blur'],
setup() {
const targetRef = ref()
const { isFocused, wrapperRef } = useFocusController(targetRef, {
disabled,
})
return () => (
<div ref={wrapperRef}>
<input ref={targetRef} />
<span>{String(isFocused.value)}</span>
</div>
)
},
})
await wrapper.find('input').trigger('focus')
expect(wrapper.emitted()).not.toHaveProperty('focus')
expect(wrapper.find('span').text()).toBe('false')
disabled.value = false
await nextTick()
await wrapper.find('input').trigger('focus')
expect(wrapper.emitted()).toHaveProperty('focus')
expect(wrapper.find('span').text()).toBe('true')
})
})

View File

@@ -1,12 +1,19 @@
import { getCurrentInstance, onMounted, ref, shallowRef, watch } from 'vue'
import {
getCurrentInstance,
onMounted,
ref,
shallowRef,
unref,
watch,
} from 'vue'
import { useEventListener } from '@vueuse/core'
import { isElement, isFocusable, isFunction } from '@element-plus/utils'
// eslint-disable-next-line no-restricted-imports
import { useFormDisabled } from '@element-plus/components/form/src/hooks/use-form-common-props' // TODO: remove this
import type { ShallowRef } from 'vue'
import type { MaybeRef } from '@vueuse/core'
interface UseFocusControllerOptions {
disabled?: MaybeRef<boolean>
/**
* return true to cancel focus
* @param event FocusEvent
@@ -24,6 +31,7 @@ interface UseFocusControllerOptions {
export function useFocusController<T extends { focus: () => void }>(
target: ShallowRef<T | undefined>,
{
disabled,
beforeFocus,
afterFocus,
beforeBlur,
@@ -33,12 +41,12 @@ export function useFocusController<T extends { focus: () => void }>(
const instance = getCurrentInstance()!
const { emit } = instance
const wrapperRef = shallowRef<HTMLElement>()
const disabled = useFormDisabled()
const isFocused = ref(false)
const handleFocus = (event: FocusEvent) => {
const cancelFocus = isFunction(beforeFocus) ? beforeFocus(event) : false
if (cancelFocus || isFocused.value) return
if (unref(disabled) || isFocused.value || cancelFocus) return
isFocused.value = true
emit('focus', event)
afterFocus?.()
@@ -47,9 +55,10 @@ export function useFocusController<T extends { focus: () => void }>(
const handleBlur = (event: FocusEvent) => {
const cancelBlur = isFunction(beforeBlur) ? beforeBlur(event) : false
if (
cancelBlur ||
unref(disabled) ||
(event.relatedTarget &&
wrapperRef.value?.contains(event.relatedTarget as Node))
wrapperRef.value?.contains(event.relatedTarget as Node)) ||
cancelBlur
)
return
@@ -60,17 +69,17 @@ export function useFocusController<T extends { focus: () => void }>(
const handleClick = (event: Event) => {
if (
(wrapperRef.value?.contains(document.activeElement) &&
wrapperRef.value !== document.activeElement) ||
unref(disabled) ||
isFocusable(event.target as HTMLElement) ||
disabled.value
(wrapperRef.value?.contains(document.activeElement) &&
wrapperRef.value !== document.activeElement)
)
return
target.value?.focus()
}
watch([wrapperRef, disabled], ([el, disabled]) => {
watch([wrapperRef, () => unref(disabled)], ([el, disabled]) => {
if (!el) return
if (disabled) {
el.removeAttribute('tabindex')