fix(components): [focus-trap] tryFocus is invalid for document.body (#19272)

* fix(components): [focus-trap] optimize tryFocus

* test: add test

* chore: remove redundant code

* test: optimize test
This commit is contained in:
qiang 2024-12-24 16:02:00 +08:00 committed by GitHub
parent 3bcad6d83d
commit 456cccdace
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 66 additions and 5 deletions

View File

@ -3,6 +3,7 @@ import {
focusFirstDescendant,
getEdges,
obtainAllFocusableElements,
tryFocus,
} from '../src/utils'
describe('focus-trap utils', () => {
@ -53,4 +54,38 @@ describe('focus-trap utils', () => {
focusFirstDescendant(focusable)
expect(document.activeElement).toBe(focusable[0])
})
describe('tryFocus', () => {
it('should be focus the input element', () => {
const input = document.querySelector('.focusable-input') as HTMLElement
tryFocus(input)
expect(document.activeElement).toBe(input)
})
it('should be focus the span element', () => {
const span = document.querySelector('.focusable-span') as HTMLElement
tryFocus(span)
expect(document.activeElement).toBe(span)
})
it('should be focus the disabled input element', () => {
const input = document.querySelector('[disabled]') as HTMLElement
tryFocus(input)
expect(document.activeElement).toBe(input)
})
it('should be focus the document body', () => {
const input = document.querySelector('.focusable-input') as HTMLElement
tryFocus(input)
expect(document.activeElement).not.toBe(document.body)
tryFocus(document.body)
expect(document.activeElement).toBe(document.body)
})
it('should be focus the null element', () => {
const activeElement = document.activeElement
tryFocus(null)
expect(document.activeElement).toBe(activeElement)
})
})
})

View File

@ -1,4 +1,5 @@
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { isElement, isFocusable } from '@element-plus/utils'
import { FOCUSOUT_PREVENTED, FOCUSOUT_PREVENTED_OPTS } from './tokens'
const focusReason = ref<'pointer' | 'keyboard'>()
@ -81,8 +82,20 @@ export const tryFocus = (
) => {
if (element && element.focus) {
const prevFocusedElement = document.activeElement
let cleanup: boolean = false
if (
isElement(element) &&
!isFocusable(element) &&
!element.getAttribute('tabindex')
) {
element.setAttribute('tabindex', '-1')
cleanup = true
}
element.focus({ preventScroll: true })
lastAutomatedFocusTimestamp.value = window.performance.now()
if (
element !== prevFocusedElement &&
isSelectable(element) &&
@ -90,6 +103,9 @@ export const tryFocus = (
) {
element.select()
}
if (isElement(element) && cleanup) {
element.removeAttribute('tabindex')
}
}
}

View File

@ -49,6 +49,7 @@ import { useNamespace, usePopperContainerId } from '@element-plus/hooks'
import { composeEventHandlers } from '@element-plus/utils'
import { ElPopperContent } from '@element-plus/components/popper'
import ElTeleport from '@element-plus/components/teleport'
import { tryFocus } from '@element-plus/components/focus-trap'
import { TOOLTIP_INJECTION_KEY } from './constants'
import { useTooltipContentProps } from './content'
import type { PopperContentInstance } from '@element-plus/components/popper'
@ -111,6 +112,7 @@ const ariaHidden = ref(true)
const onTransitionLeave = () => {
onHide()
isFocusInsideContent() && tryFocus(document.body)
ariaHidden.value = true
}
@ -161,6 +163,14 @@ const onBlur = () => {
}
}
const isFocusInsideContent = (event?: FocusEvent) => {
const popperContent: HTMLElement | undefined =
contentRef.value?.popperContentRef
const activeElement = (event?.relatedTarget as Node) || document.activeElement
return popperContent?.contains(activeElement)
}
watch(
() => unref(open),
(val) => {
@ -187,5 +197,9 @@ defineExpose({
* @description el-popper-content component instance
*/
contentRef,
/**
* @description validate current focus event is trigger inside el-popper-content
*/
isFocusInsideContent,
})
</script>

View File

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