FOCUSABLE_ELEMENT_SELECTORS
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])`
isVisible
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
}
/*
getComputedStyle 获取到dom结构的样式对象集合
offsetParent
元素自身有fixed定位,父元素不存在定位,则offsetParent的结果未null
元素自身无fixed定位,且父元素不存在定位,offsetParent为 body元素
元素自身无fixed定位,且父元素存在定位,offsetParent为离自身最近且经过定位的父元素
body 元素的offsetParent是null
*/
解析
getComputedStyle 获取指定dom元素的样式属性结合
返回的是一个对象 通过属性名称即可访问
如 getComputedStyle(document.getElementById("dom"))['width] 即可访问到dom元素的宽度
offsetParent 距离当前元素最近的进行过定位的父元素
素自身有fixed定位,父元素不存在定位,则offsetParent的结果未null
元素自身无fixed定位,且父元素不存在定位,offsetParent为 body元素
元素自身无fixed定位,且父元素存在定位,offsetParent为离自身最近且经过定位的父元素
body 元素的offsetParent是null
obtainAllFocusableElements
export const obtainAllFocusableElements = (
element: HTMLElement
): HTMLElement[] => {
return Array.from(
element.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENT_SELECTORS)
).filter((item: HTMLElement) => isFocusable(item) && isVisible(item))
}
/*
obtainAllFocusableElements
入参
HTMLElement 单个dom元素
返回值
HTMLElement [] dom元素集合的 数组
Array.from 将伪数组转换为真实的数组
element.querySelectorAll 查找当前元素下的所有 满足条件的子元素 FOCUSABLE_ELEMENT_SELECTORS(子元素类名集合)
filter 过滤
isFocusable 和 isVisible 调用处理都返回true的
*/
obtainAllFocusableElements
入参
HTMLElement 单个dom元素
返回值
HTMLElement [] dom元素集合的 数组
Array.from 将伪数组转换为真实的数组
element.querySelectorAll 查找当前元素下的所有 满足条件的子元素 FOCUSABLE_ELEMENT_SELECTORS(子元素类名集合)
filter 过滤
isFocusable 和 isVisible 调用处理都返回true的
export const isFocusable = (element: HTMLElement): boolean => {
if (
element.tabIndex > 0 ||
(element.tabIndex === 0 && element.getAttribute('tabIndex') !== null)
) {
return true
}
// HTMLButtonElement has disabled
if ((element as HTMLButtonElement).disabled) {
// HTMLButtonElement 表示按钮元素类型
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'
)
/*
!! (element as HTMLAnchorElement).href
!! 双重取反
!!a 表示 a != ""&&a != undefined && a != null
a.rel 用于指定当前文档与被链接文档的关系
HTMLAnchorElement 表示锚点链接类型
*/
}
case 'INPUT': {
return !(
(element as HTMLInputElement).type === 'hidden' ||
(element as HTMLInputElement).type === 'file'
)
/*
HTMLInputElement 输入元素类型
as typescript 中的类型断言
所谓类型断言指的是我知道 某个值的详细信息
*/
}
case 'BUTTON':
case 'SELECT':
case 'TEXTAREA': {
return true
}
default: {
return false
}
}
}
tabIndex
tabIndex 是全局属性,表示元素是否可以通过键盘导航 tab 选中
如果 tabIndex 为 负值 小于 0 则不可以通过 tab选中
tabIndex 设置为0 时 通常是为不可聚焦元素添加可聚焦属性
tabIndex 为正数 大于 0 时 可以调整 被访问的优先级
(element as HTMLButtonElement).disabled
as 是断言 此处的作用是 认定了 element的类型为 HTMLButtonElement元素
此处表达的是 如果 button元素有disabled属性为true 即 终止代码 不可获取焦点
!! 双重取反
!!a 等价于 a != "" && a != null && a != undefined
a.rel
a.rel 用于指定当前文档于被链接文档的关系
attemptFocus
export const attemptFocus = (element: HTMLElement): boolean => {
if (!isFocusable(element)) {
// 判断是否可以获取焦点 入宫不可获取焦点就返回false
return false
}
// Remove the old try catch block since there will be no error to be thrown
element.focus?.()
// ?. 表示 问号之前的成立才执行问号之后的代码
return document.activeElement === element
// document.activeElement 当前页面中获取焦点的元素
}
element.focus?.()
element.focus?.() 表示 入宫 element的focus属性存在就调用
相当于三元表达式 element.focus?element.focus():null
document.activeElement
document.activeElement 表示当前页面中获取焦点的元素
document.activeElement = element 表示把当前的 element 设置为获取 焦点的元素
triggerEvent 自定义事件
export const triggerEvent = function (
elm: HTMLElement,//当前dom元素
name: string,//事件的名称
...opts: Array<boolean>//更多参数 opts,为一个数组, true 表示阻止事件冒泡 true 表示阻止默认行为
): HTMLElement {
let eventName: string
if (name.includes('mouse') || name.includes('click')) {
// includes 表示是否包含 返回true或者false mouse 或者 click 都是鼠标事件 MouseEvents
eventName = 'MouseEvents'
} else if (name.includes('key')) {
// key表示键盘事件
eventName = 'KeyboardEvent'
} else {
// 既不是鼠标事件也不是键盘事件
eventName = 'HTMLEvents'
}
const evt = document.createEvent(eventName)
// 创建事件
evt.initEvent(name, ...opts)
// 初始化事件
// opts 是数组 boolean 值 第一个表示是否阻止冒泡 是否阻止默认行为
// document.createEvent dispatchEvent
elm.dispatchEvent(evt)//触发事件
/*
在其他地方可用document.addEventListener("事件名称") 来监听这些自定义事件
*/
return elm
}
....opts:Array<boolean>
表示入参 opts 是一个类型为boolean的数组
并将其解构出来
document.createEvent(eventName)
document.createEvent 创建自定义事件
并传入事件类型 eventName
eventName类型
MouseEvents
鼠标事件
KeyboardEvent
键盘事件
HTMLEvents
html事件 非 鼠标和键盘事件
自定义事件
let evt = document.createEvent(eventName) 创建自定义事件 并传入事件类型
evt.initEvent(name,...opts) 初始化事件 并传入参数 自定义事件名称 和 是否阻止冒泡 是否阻止默认事件行为 阻止参数
ele.dispatchEvent(evt) 触发自定义事件
可通过 document.addEventListener(name,()=>{}) 监听自定义事件的触发
getSibling 获取到指定的兄弟元素
export const getSibling = (
el: HTMLElement,//当前元素
distance: number,//距离当前元素的位置
elClass: string//元素的类名
) => {
const { parentNode } = el//拿到当前元素的父元素
if (!parentNode) return null//如果当前元素没有父元素就是 最顶级的元素 没有兄弟元素 就返回null
const siblings = parentNode.querySelectorAll(elClass)//获取到所有兄弟元素的集合
const index = Array.prototype.indexOf.call(siblings, el)// 获取到当前元素在所有兄弟元素集合中的位置 索引
return siblings[index + distance] || null// 返回当前元素 距离兄弟元素指定位置的元素
}
思路
- 获取到当前元素的父元素
- 如果父元素不存在,就返回null
- 根据类名查找到所有的元素
- 拿到当前元素的位置索引
- 通过 当前元素的索引和传入的distance相加即得到指定坐标的元素
Array.prototype.indexOf.call(siblings, el)
查找当前元素 el 在 siblings 的位置
由于sibilings是伪数组 所以需要调用 Array.prototype.indexof.call
等价于 Array.from(siblings).indexOf(el) Array.from 可以将伪数组转化为数组
完整代码
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])`
/**
* 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
}
/*
getComputedStyle 获取到dom结构的样式对象集合
offsetParent
元素自身有fixed定位,父元素不存在定位,则offsetParent的结果未null
元素自身无fixed定位,且父元素不存在定位,offsetParent为 body元素
元素自身无fixed定位,且父元素存在定位,offsetParent为离自身最近且经过定位的父元素
body 元素的offsetParent是null
*/
export const obtainAllFocusableElements = (
element: HTMLElement
): HTMLElement[] => {
return Array.from(
element.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENT_SELECTORS)
).filter((item: HTMLElement) => isFocusable(item) && isVisible(item))
}
/*
obtainAllFocusableElements
入参
HTMLElement 单个dom元素
返回值
HTMLElement [] dom元素集合的 数组
Array.from 将伪数组转换为真实的数组
element.querySelectorAll 查找当前元素下的所有 满足条件的子元素 FOCUSABLE_ELEMENT_SELECTORS(子元素类名集合)
filter 过滤
isFocusable 和 isVisible 调用处理都返回true的
*/
/**
* @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
}
// HTMLButtonElement has disabled
if ((element as HTMLButtonElement).disabled) {
// HTMLButtonElement 表示按钮元素类型
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'
)
/*
!! (element as HTMLAnchorElement).href
!! 双重取反
!!a 表示 a != ""&&a != undefined && a != null
a.rel 用于指定当前文档与被链接文档的关系
HTMLAnchorElement 表示锚点链接类型
*/
}
case 'INPUT': {
return !(
(element as HTMLInputElement).type === 'hidden' ||
(element as HTMLInputElement).type === 'file'
)
/*
HTMLInputElement 输入元素类型
as typescript 中的类型断言
所谓类型断言指的是我知道 某个值的详细信息
*/
}
case 'BUTTON':
case 'SELECT':
case 'TEXTAREA': {
return true
}
default: {
return false
}
}
}
/**
* @desc Set Attempt to set focus on the current node.
* @param element
* The node to attempt to focus on.
* @returns
* true if element is focused.
*/
export const attemptFocus = (element: HTMLElement): boolean => {
if (!isFocusable(element)) {
// 判断是否可以获取焦点 入宫不可获取焦点就返回false
return false
}
// Remove the old try catch block since there will be no error to be thrown
element.focus?.()
// ?. 表示 问号之前的成立才执行问号之后的代码
return document.activeElement === element
// document.activeElement 当前页面中获取焦点的元素
}
/**
* Trigger an event
* mouseenter, mouseleave, mouseover, keyup, change, click, etc.
* @param {HTMLElement} elm
* @param {String} name
* @param {*} opts
*/
export const = function (
elm: HTMLElement,//当前dom元素
name: string,//事件的名称
...opts: Array<boolean>//更多参数 opts,为一个数组, true 表示阻止事件冒泡 true 表示阻止默认行为
): HTMLElement {
let eventName: string
if (name.includes('mouse') || name.includes('click')) {
// includes 表示是否包含 返回true或者false mouse 或者 click 都是鼠标事件 MouseEvents
eventName = 'MouseEvents'
} else if (name.includes('key')) {
// key表示键盘事件
eventName = 'KeyboardEvent'
} else {
// 既不是鼠标事件也不是键盘事件
eventName = 'HTMLEvents'
}
const evt = document.createEvent(eventName)
// 创建事件
evt.initEvent(name, ...opts)
// 初始化事件
// opts 是数组 boolean 值 第一个表示是否阻止冒泡 是否阻止默认行为
// document.createEvent dispatchEvent
elm.dispatchEvent(evt)//触发事件
/*
在其他地方可用document.addEventListener("事件名称") 来监听这些自定义事件
*/
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//如果当前元素没有父元素就是 最顶级的元素 没有兄弟元素 就返回null
const siblings = parentNode.querySelectorAll(elClass)//获取到所有兄弟元素的集合
const index = Array.prototype.indexOf.call(siblings, el)// 获取到当前元素在所有兄弟元素集合中的位置 索引
return siblings[index + distance] || null// 返回当前元素 距离兄弟元素指定位置的元素
}
export const focusNode = (el: HTMLElement) => {
if (!el) return
el.focus()
!isLeaf(el) && el.click()
}