Files
element-plus/packages/components/focus-trap/src/utils.ts
sea 1f7cdc3915 chore: only show warnings in dev mode (#22675)
* chore: only show warnings in dev mode

* chore: test

* chore: test2

* chore: test3

* chore: test3

* chore: update 4

* chore: update

* chore: update

* chore: update

* chore: update

Co-authored-by: btea <2356281422@qq.com>

* chore: add security checks to all process

---------

Co-authored-by: btea <2356281422@qq.com>
2025-11-10 09:25:01 +08:00

201 lines
5.1 KiB
TypeScript

import { onBeforeUnmount, onMounted, ref } from 'vue'
import { focusElement } from '@element-plus/utils'
import { FOCUSOUT_PREVENTED, FOCUSOUT_PREVENTED_OPTS } from './tokens'
const focusReason = ref<'pointer' | 'keyboard'>()
const lastUserFocusTimestamp = ref<number>(0)
const lastAutomatedFocusTimestamp = ref<number>(0)
let focusReasonUserCount = 0
export type FocusLayer = {
paused: boolean
pause: () => void
resume: () => void
}
export type FocusStack = FocusLayer[]
export const obtainAllFocusableElements = (
element: HTMLElement
): HTMLElement[] => {
const nodes: HTMLElement[] = []
const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT, {
acceptNode: (
node: Element & {
disabled: boolean
hidden: boolean
type: string
tabIndex: number
}
) => {
const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden'
if (node.disabled || node.hidden || isHiddenInput)
return NodeFilter.FILTER_SKIP
return node.tabIndex >= 0 || node === document.activeElement
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP
},
})
while (walker.nextNode()) nodes.push(walker.currentNode as HTMLElement)
return nodes
}
export const getVisibleElement = (
elements: HTMLElement[],
container: HTMLElement
) => {
for (const element of elements) {
if (!isHidden(element, container)) return element
}
}
export const isHidden = (element: HTMLElement, container: HTMLElement) => {
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test')
return false
if (getComputedStyle(element).visibility === 'hidden') return true
while (element) {
if (container && element === container) return false
if (getComputedStyle(element).display === 'none') return true
element = element.parentElement as HTMLElement
}
return false
}
export const getEdges = (container: HTMLElement) => {
const focusable = obtainAllFocusableElements(container)
const first = getVisibleElement(focusable, container)
const last = getVisibleElement(focusable.reverse(), container)
return [first, last]
}
const isSelectable = (
element: any
): element is HTMLInputElement & { select: () => void } => {
return element instanceof HTMLInputElement && 'select' in element
}
export const tryFocus = (
element?: HTMLElement | { focus: () => void } | null,
shouldSelect?: boolean
) => {
if (element) {
const prevFocusedElement = document.activeElement
focusElement(element, { preventScroll: true })
lastAutomatedFocusTimestamp.value = window.performance.now()
if (
element !== prevFocusedElement &&
isSelectable(element) &&
shouldSelect
) {
element.select()
}
}
}
function removeFromStack<T>(list: T[], item: T) {
const copy = [...list]
const idx = list.indexOf(item)
if (idx !== -1) {
copy.splice(idx, 1)
}
return copy
}
const createFocusableStack = () => {
let stack = [] as FocusStack
const push = (layer: FocusLayer) => {
const currentLayer = stack[0]
if (currentLayer && layer !== currentLayer) {
currentLayer.pause()
}
stack = removeFromStack(stack, layer)
stack.unshift(layer)
}
const remove = (layer: FocusLayer) => {
stack = removeFromStack(stack, layer)
stack[0]?.resume?.()
}
return {
push,
remove,
}
}
export const focusFirstDescendant = (
elements: HTMLElement[],
shouldSelect = false
) => {
const prevFocusedElement = document.activeElement
for (const element of elements) {
tryFocus(element, shouldSelect)
if (document.activeElement !== prevFocusedElement) return
}
}
export const focusableStack = createFocusableStack()
export const isFocusCausedByUserEvent = (): boolean => {
return lastUserFocusTimestamp.value > lastAutomatedFocusTimestamp.value
}
const notifyFocusReasonPointer = () => {
focusReason.value = 'pointer'
lastUserFocusTimestamp.value = window.performance.now()
}
const notifyFocusReasonKeydown = () => {
focusReason.value = 'keyboard'
lastUserFocusTimestamp.value = window.performance.now()
}
export const useFocusReason = (): {
focusReason: typeof focusReason
lastUserFocusTimestamp: typeof lastUserFocusTimestamp
lastAutomatedFocusTimestamp: typeof lastAutomatedFocusTimestamp
} => {
onMounted(() => {
if (focusReasonUserCount === 0) {
document.addEventListener('mousedown', notifyFocusReasonPointer)
document.addEventListener('touchstart', notifyFocusReasonPointer)
document.addEventListener('keydown', notifyFocusReasonKeydown)
}
focusReasonUserCount++
})
onBeforeUnmount(() => {
focusReasonUserCount--
if (focusReasonUserCount <= 0) {
document.removeEventListener('mousedown', notifyFocusReasonPointer)
document.removeEventListener('touchstart', notifyFocusReasonPointer)
document.removeEventListener('keydown', notifyFocusReasonKeydown)
}
})
return {
focusReason,
lastUserFocusTimestamp,
lastAutomatedFocusTimestamp,
}
}
export const createFocusOutPreventedEvent = (
detail: CustomEventInit['detail']
) => {
return new CustomEvent(FOCUSOUT_PREVENTED, {
...FOCUSOUT_PREVENTED_OPTS,
detail,
})
}