element-plus/packages/utils/aria.ts

187 lines
5.1 KiB
TypeScript
Raw Normal View History

import type { Nullable } from './types'
export const EVENT_CODE = {
tab: 'Tab',
enter: 'Enter',
space: 'Space',
Feat/datepicker && datetimepicker (#326) * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * chore: update * Feat/form (#342) * feat(form): add form component fix #125 * test(form): add test code * docs(form): add form doc * feat: add uitls merge * fix(form): fix style * test(form): add form test code * refactor(form): review changes * test(form): use idiomatic vue-test-util methods * feat(core): bump vue version * feat(form): rewrite label wrap * feat(form): fix tons of bugs * fix(form): reuse ts extension * refactor(form): move out label width computation * fix(form): fix tons of bugs * fix(form): test Co-authored-by: 286506460 <286506460@qq.com> * Feat/select (#381) * fix: resove conflict * feat: select basic usage * feat: select basic usage * feat: select feature create item * fix: fix option data insert * refactor: select * fix: fix parse error * feat: select test * fix: select add popper * fix: update select option * fix: add select dependency * fix: add index.ts file * fix(select): clean up * fix(select): some refactor * fix(select): some update * fix(select): fix all test cases Co-authored-by: helen <yinhelen.hlj@qq.com> Co-authored-by: Herrington Darkholme <2883231+HerringtonDarkholme@users.noreply.github.com> Co-authored-by: 286506460 <286506460@qq.com> Co-authored-by: helen <yinhelen.hlj@qq.com>
2020-10-03 20:13:19 +08:00
left: 'ArrowLeft', // 37
up: 'ArrowUp', // 38
right: 'ArrowRight', // 39
down: 'ArrowDown', // 40
esc: 'Escape',
delete: 'Delete',
backspace: 'Backspace',
numpadEnter: 'NumpadEnter',
pageUp: 'PageUp',
pageDown: 'PageDown',
home: 'Home',
end: 'End',
}
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
}
export const obtainAllFocusableElements = (
element: HTMLElement
): HTMLElement[] => {
return Array.from(
element.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENT_SELECTORS)
).filter((item: HTMLElement) => isFocusable(item) && isVisible(item))
}
2020-07-28 17:48:40 +08:00
/**
* @desc Determine if target element is focusable
* @param element {HTMLElement}
* @returns {Boolean} true if it is focusable
*/
export const isFocusable = (element: HTMLElement): boolean => {
2020-07-28 17:48:40 +08:00
if (
element.tabIndex > 0 ||
(element.tabIndex === 0 && element.getAttribute('tabIndex') !== null)
) {
return true
}
// HTMLButtonElement has disabled
if ((element as HTMLButtonElement).disabled) {
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'
)
2020-07-28 17:48:40 +08:00
}
case 'INPUT': {
return !(
(element as HTMLInputElement).type === 'hidden' ||
(element as HTMLInputElement).type === 'file'
)
2020-07-28 17:48:40 +08:00
}
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 => {
2020-07-28 17:48:40 +08:00
if (!isFocusable(element)) {
return false
}
Utils.IgnoreUtilFocusChanges = true
// Remove the old try catch block since there will be no error to be thrown
element.focus?.()
2020-07-28 17:48:40 +08:00
Utils.IgnoreUtilFocusChanges = false
return document.activeElement === element
}
/**
* 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 {
2020-07-28 17:48:40 +08:00
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
): Nullable<Element> => {
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 focusNode = (el) => {
if (!el) return
el.focus()
!isLeaf(el) && el.click()
}
2020-07-28 17:48:40 +08:00
const Utils = {
IgnoreUtilFocusChanges: false,
/**
* @desc Set focus on descendant nodes until the first focusable element is
* found.
* @param {HTMLElement} element
* DOM node for which to find the first focusable descendant.
* @returns {Boolean}
* true if a focusable element is found and focus is set.
*/
focusFirstDescendant(element: HTMLElement): boolean {
2020-07-28 17:48:40 +08:00
for (let i = 0; i < element.childNodes.length; i++) {
const child = element.childNodes[i] as HTMLElement
if (attemptFocus(child) || this.focusFirstDescendant(child)) {
2020-07-28 17:48:40 +08:00
return true
}
}
return false
},
/**
* @desc Find the last descendant node that is focusable.
* @param {HTMLElement} element
* DOM node for which to find the last focusable descendant.
* @returns {Boolean}
* true if a focusable element is found and focus is set.
*/
focusLastDescendant(element: HTMLElement): boolean {
2020-07-28 17:48:40 +08:00
for (let i = element.childNodes.length - 1; i >= 0; i--) {
const child = element.childNodes[i] as HTMLElement
if (attemptFocus(child) || this.focusLastDescendant(child)) {
2020-07-28 17:48:40 +08:00
return true
}
}
return false
},
}
export default Utils