diff --git a/packages/components/focus-trap/__tests__/utils.test.ts b/packages/components/focus-trap/__tests__/utils.test.ts index fcc8488227..ae2a7a33bf 100644 --- a/packages/components/focus-trap/__tests__/utils.test.ts +++ b/packages/components/focus-trap/__tests__/utils.test.ts @@ -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) + }) + }) }) diff --git a/packages/components/focus-trap/src/utils.ts b/packages/components/focus-trap/src/utils.ts index 9f7de1341b..0cf9ede11c 100644 --- a/packages/components/focus-trap/src/utils.ts +++ b/packages/components/focus-trap/src/utils.ts @@ -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') + } } } diff --git a/packages/components/tooltip/src/content.vue b/packages/components/tooltip/src/content.vue index 5ada454b7a..6ea329f1ee 100644 --- a/packages/components/tooltip/src/content.vue +++ b/packages/components/tooltip/src/content.vue @@ -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, }) diff --git a/packages/components/tooltip/src/tooltip.vue b/packages/components/tooltip/src/tooltip.vue index 114bd935bf..d1886f709f 100644 --- a/packages/components/tooltip/src/tooltip.vue +++ b/packages/components/tooltip/src/tooltip.vue @@ -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())