Files
element-plus/packages/utils/dom/aria.ts
yinfoyuan c417dfb3ed feat(utils): [dom] add support for ShadowDom (#23235)
Add the isShadowRoot utility function to detect ShadowRoot
handle ShadowRoot situations in getStyle and getScrollContainer
add unit tests for isShadowRoot

BREAKING CHANGE: `getScrollContainer()` now returns shadowRoot

closed #23228

Co-authored-by: yinfoyuan <1063428973@qq.com>
2026-02-01 14:07:11 +08:00

143 lines
3.7 KiB
TypeScript

const FOCUSABLE_ELEMENT_SELECTORS = `a[href],button:not([disabled]),button:not([hidden]),:not([tabindex="-1"]),input:not([disabled]),input:not([type="hidden"]),select:not([disabled]),textarea:not([disabled])`
export const isShadowRoot = (e: unknown): e is ShadowRoot => {
if (typeof ShadowRoot === 'undefined') return false
return e instanceof ShadowRoot
}
const isHTMLElement = (e: unknown): e is Element => {
if (typeof Element === 'undefined') return false
return e instanceof Element
}
/**
* Determine if the testing element is visible on screen no matter if its on the viewport or not
*/
export const isVisible = (element: HTMLElement) => {
if (process.env.NODE_ENV === 'test') return true
const computed = getComputedStyle(element)
// element.offsetParent won't work on fix positioned
// WARNING: potential issue here, going to need some expert advices on this issue
return computed.position === 'fixed' ? false : element.offsetParent !== null
}
export const obtainAllFocusableElements = (
element: HTMLElement
): HTMLElement[] => {
return Array.from(
element.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENT_SELECTORS)
).filter((item: HTMLElement) => isFocusable(item) && isVisible(item))
}
/**
* @desc Determine if target element is focusable
* @param element {HTMLElement}
* @returns {Boolean} true if it is focusable
*/
export const isFocusable = (element: HTMLElement): boolean => {
if (
element.tabIndex > 0 ||
(element.tabIndex === 0 && element.getAttribute('tabIndex') !== null)
) {
return true
}
if (
element.tabIndex < 0 ||
element.hasAttribute('disabled') ||
element.getAttribute('aria-disabled') === 'true'
) {
return false
}
switch (element.nodeName) {
case 'A': {
// casting current element to Specific HTMLElement in order to be more type precise
return (
!!(element as HTMLAnchorElement).href &&
(element as HTMLAnchorElement).rel !== 'ignore'
)
}
case 'INPUT': {
return !(
(element as HTMLInputElement).type === 'hidden' ||
(element as HTMLInputElement).type === 'file'
)
}
case 'BUTTON':
case 'SELECT':
case 'TEXTAREA': {
return true
}
default: {
return false
}
}
}
/**
* Trigger an event
* mouseenter, mouseleave, mouseover, keyup, change, click, etc.
* @param {HTMLElement} elm
* @param {String} name
* @param {*} opts
*/
export const triggerEvent = function (
elm: HTMLElement,
name: string,
...opts: Array<boolean>
): HTMLElement {
let eventName: string
if (name.includes('mouse') || name.includes('click')) {
eventName = 'MouseEvents'
} else if (name.includes('key')) {
eventName = 'KeyboardEvent'
} else {
eventName = 'HTMLEvents'
}
const evt = document.createEvent(eventName)
evt.initEvent(name, ...opts)
elm.dispatchEvent(evt)
return elm
}
export const isLeaf = (el: HTMLElement) => !el.getAttribute('aria-owns')
export const getSibling = (
el: HTMLElement,
distance: number,
elClass: string
) => {
const { parentNode } = el
if (!parentNode) return null
const siblings = parentNode.querySelectorAll(elClass)
const index = Array.prototype.indexOf.call(siblings, el)
return siblings[index + distance] || null
}
export const focusElement = (
el?: HTMLElement | { focus: () => void } | null,
options?: FocusOptions
) => {
if (!el || !el.focus) return
let cleanup: boolean = false
if (isHTMLElement(el) && !isFocusable(el) && !el.getAttribute('tabindex')) {
el.setAttribute('tabindex', '-1')
cleanup = true
}
el.focus(options)
if (isHTMLElement(el) && cleanup) {
el.removeAttribute('tabindex')
}
}
export const focusNode = (el: HTMLElement) => {
if (!el) return
focusElement(el)
!isLeaf(el) && el.click()
}