mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-11-29 17:58:08 +08:00
Migrate utilities
This commit is contained in:
parent
5b98819a85
commit
3bdc7a8496
24
packages/utils/after-leave.ts
Normal file
24
packages/utils/after-leave.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ComponentPublicInstance } from 'vue'
|
||||
/**
|
||||
* Bind after-leave event for vue instance. Make sure after-leave is called in any browsers.
|
||||
*/
|
||||
export default function(instance: ComponentPublicInstance, callback: (...args: unknown[]) => unknown, speed = 300): void {
|
||||
if (!instance || !callback) throw new Error('instance & callback is required')
|
||||
let called = false
|
||||
const afterLeaveCallback = function(...args: unknown[]) {
|
||||
if (called) return
|
||||
called = true
|
||||
if (callback) {
|
||||
callback(args)
|
||||
}
|
||||
}
|
||||
// TODO: migrate to [mitt](https://github.com/developit/mitt)
|
||||
// if (once) {
|
||||
// instance.$once('after-leave', afterLeaveCallback)
|
||||
// } else {
|
||||
// instance.$on('after-leave', afterLeaveCallback)
|
||||
// }
|
||||
setTimeout(() => {
|
||||
afterLeaveCallback()
|
||||
}, speed + 100)
|
||||
}
|
91
packages/utils/aria-dialog.ts
Normal file
91
packages/utils/aria-dialog.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import Utils from './aria'
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @desc Dialog object providing modal focus management.
|
||||
*
|
||||
* Assumptions: The element serving as the dialog container is present in the
|
||||
* DOM and hidden. The dialog container has role='dialog'.
|
||||
*
|
||||
* @param dialogId
|
||||
* The ID of the element serving as the dialog container.
|
||||
* @param focusAfterClosed
|
||||
* Either the DOM node or the ID of the DOM node to focus when the
|
||||
* dialog closes.
|
||||
* @param focusFirst
|
||||
* Optional parameter containing either the DOM node or the ID of the
|
||||
* DOM node to focus when the dialog opens. If not specified, the
|
||||
* first focusable element in the dialog will receive focus.
|
||||
*/
|
||||
let tabEvent: EventHandlerNonNull
|
||||
|
||||
class Dialog {
|
||||
public lastFocus: Nullable<Element> = null;
|
||||
constructor(public dialogNode: Nullable<HTMLElement>, public focusAfterClosed: HTMLElement, public focusFirst: HTMLElement | string) {
|
||||
if (this.dialogNode === null || this.dialogNode.getAttribute('role') !== 'dialog') {
|
||||
throw new Error('Dialog() requires a DOM element with ARIA role of dialog.')
|
||||
}
|
||||
|
||||
if (typeof focusAfterClosed === 'string') {
|
||||
this.focusAfterClosed = document.getElementById(focusAfterClosed)
|
||||
} else if (typeof focusAfterClosed === 'object') {
|
||||
this.focusAfterClosed = focusAfterClosed
|
||||
} else {
|
||||
this.focusAfterClosed = null
|
||||
}
|
||||
|
||||
if (typeof focusFirst === 'string') {
|
||||
this.focusFirst = document.getElementById(focusFirst)
|
||||
} else if (typeof focusFirst === 'object') {
|
||||
this.focusFirst = focusFirst
|
||||
} else {
|
||||
this.focusFirst = null
|
||||
}
|
||||
|
||||
if (this.focusFirst) {
|
||||
(this.focusFirst as HTMLElement).focus()
|
||||
} else {
|
||||
Utils.focusFirstDescendant(this.dialogNode)
|
||||
}
|
||||
|
||||
this.lastFocus = document.activeElement
|
||||
tabEvent = (e: KeyboardEvent) => {
|
||||
this.trapFocus(e)
|
||||
}
|
||||
this.addListeners()
|
||||
}
|
||||
|
||||
addListeners(): void {
|
||||
document.addEventListener('focus', tabEvent, true)
|
||||
}
|
||||
|
||||
removeListeners(): void {
|
||||
document.removeEventListener('focus', tabEvent, true)
|
||||
}
|
||||
|
||||
closeDialog(): void {
|
||||
this.removeListeners()
|
||||
if (this.focusAfterClosed) {
|
||||
setTimeout(() => {
|
||||
this.focusAfterClosed.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
trapFocus(event: KeyboardEvent): void {
|
||||
if (Utils.IgnoreUtilFocusChanges) {
|
||||
return
|
||||
}
|
||||
if (this.dialogNode.contains(event.target as Node)) {
|
||||
this.lastFocus = event.target as HTMLElement
|
||||
} else {
|
||||
Utils.focusFirstDescendant(this.dialogNode)
|
||||
if (this.lastFocus === document.activeElement) {
|
||||
Utils.focusLastDescendant(this.dialogNode)
|
||||
}
|
||||
this.lastFocus = document.activeElement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Dialog
|
139
packages/utils/aria.ts
Normal file
139
packages/utils/aria.ts
Normal file
@ -0,0 +1,139 @@
|
||||
|
||||
/**
|
||||
* @desc Determine if target element is focusable
|
||||
* @param element {HTMLElement}
|
||||
* @returns {Boolean} true if it is focusable
|
||||
*/
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
const attemptFocus = (element: HTMLElement): boolean => {
|
||||
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 && element.focus()
|
||||
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
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
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: function(element: HTMLElement): boolean {
|
||||
for (let i = 0; i < element.childNodes.length; i++) {
|
||||
const child = element.childNodes[i]
|
||||
if (
|
||||
attemptFocus(child as HTMLElement) ||
|
||||
this.focusFirstDescendant(child)
|
||||
) {
|
||||
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: function(element: HTMLElement): boolean {
|
||||
for (let i = element.childNodes.length - 1; i >= 0; i--) {
|
||||
const child = element.childNodes[i]
|
||||
if (
|
||||
attemptFocus(child as HTMLElement) ||
|
||||
this.focusLastDescendant(child)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
const eventKeys = {
|
||||
tab: 9,
|
||||
enter: 13,
|
||||
space: 32,
|
||||
left: 37,
|
||||
up: 38,
|
||||
right: 39,
|
||||
down: 40,
|
||||
esc: 27,
|
||||
}
|
||||
|
||||
export default Utils
|
||||
export {
|
||||
eventKeys,
|
||||
triggerEvent,
|
||||
}
|
15
packages/utils/config.ts
Normal file
15
packages/utils/config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
const $ELEMENT: Record<string, unknown> = { }
|
||||
|
||||
const setConfig = (key: string, value: unknown): void => {
|
||||
$ELEMENT[key] = value
|
||||
}
|
||||
|
||||
const getConfig = (key: string): unknown => {
|
||||
return $ELEMENT[key]
|
||||
}
|
||||
|
||||
export {
|
||||
getConfig,
|
||||
setConfig,
|
||||
}
|
15
packages/utils/menu/menu-bar.ts
Normal file
15
packages/utils/menu/menu-bar.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import MenuItem from './menu-item'
|
||||
|
||||
|
||||
class Menu {
|
||||
constructor(public domNode: Node) {
|
||||
this.init()
|
||||
}
|
||||
init(): void {
|
||||
const menuChildren = this.domNode.childNodes;
|
||||
[].filter.call(menuChildren, (child: Node) => child.nodeType === 1).forEach((child: Node) => {
|
||||
new MenuItem(child as HTMLElement)
|
||||
})
|
||||
}
|
||||
}
|
||||
export default Menu
|
57
packages/utils/menu/menu-item.ts
Normal file
57
packages/utils/menu/menu-item.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import {eventKeys, triggerEvent} from '../aria'
|
||||
import SubMenu from './submenu'
|
||||
|
||||
|
||||
class MenuItem {
|
||||
public submenu: SubMenu = null
|
||||
constructor(public domNode: HTMLElement) {
|
||||
this.submenu = null
|
||||
this.init()
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.domNode.setAttribute('tabindex', '0')
|
||||
const menuChild = this.domNode.querySelector('.el-menu')
|
||||
if (menuChild) {
|
||||
this.submenu = new SubMenu(this, menuChild)
|
||||
}
|
||||
this.addListeners()
|
||||
}
|
||||
|
||||
addListeners(): void {
|
||||
const keys = eventKeys
|
||||
this.domNode.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
let prevDef = false
|
||||
switch (event.keyCode) {
|
||||
case keys.down: {
|
||||
triggerEvent(event.currentTarget as HTMLElement, 'mouseenter')
|
||||
this.submenu && this.submenu.gotoSubIndex(0)
|
||||
prevDef = true
|
||||
break
|
||||
}
|
||||
case keys.up: {
|
||||
triggerEvent(event.currentTarget as HTMLElement, 'mouseenter')
|
||||
this.submenu && this.submenu.gotoSubIndex(this.submenu.subMenuItems.length - 1)
|
||||
prevDef = true
|
||||
break
|
||||
}
|
||||
case keys.tab: {
|
||||
triggerEvent(event.currentTarget as HTMLElement, 'mouseleave')
|
||||
break
|
||||
}
|
||||
case keys.enter:
|
||||
case keys.space: {
|
||||
prevDef = true
|
||||
|
||||
(event.currentTarget as HTMLElement).click()
|
||||
break
|
||||
}
|
||||
}
|
||||
if (prevDef) {
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default MenuItem
|
65
packages/utils/menu/submenu.ts
Normal file
65
packages/utils/menu/submenu.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import {eventKeys, triggerEvent } from '../aria'
|
||||
import MenuItem from './menu-item'
|
||||
|
||||
class SubMenu {
|
||||
public subMenuItems: NodeList
|
||||
public subIndex = 0
|
||||
constructor(public parent: MenuItem, public domNode: ParentNode) {
|
||||
this.subIndex = 0
|
||||
this.init()
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.subMenuItems = this.domNode.querySelectorAll('li')
|
||||
this.addListeners()
|
||||
}
|
||||
|
||||
gotoSubIndex(idx: number): void {
|
||||
if (idx === this.subMenuItems.length) {
|
||||
idx = 0
|
||||
} else if (idx < 0) {
|
||||
idx = this.subMenuItems.length - 1
|
||||
}
|
||||
(this.subMenuItems[idx] as HTMLElement).focus()
|
||||
this.subIndex = idx
|
||||
}
|
||||
|
||||
addListeners(): void {
|
||||
const keys = eventKeys
|
||||
const parentNode = this.parent.domNode
|
||||
Array.prototype.forEach.call(this.subMenuItems, (el: Element) => {
|
||||
el.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
let prevDef = false
|
||||
switch (event.keyCode) {
|
||||
case keys.down: {
|
||||
this.gotoSubIndex(this.subIndex + 1)
|
||||
prevDef = true
|
||||
break
|
||||
}
|
||||
case keys.up: {
|
||||
this.gotoSubIndex(this.subIndex - 1)
|
||||
prevDef = true
|
||||
break
|
||||
}
|
||||
case keys.tab: {
|
||||
triggerEvent(parentNode as HTMLElement, 'mouseleave')
|
||||
break
|
||||
}
|
||||
case keys.enter:
|
||||
case keys.space: {
|
||||
prevDef = true
|
||||
(event.currentTarget as HTMLElement).click()
|
||||
break
|
||||
}
|
||||
}
|
||||
if (prevDef) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
return false
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default SubMenu
|
@ -145,7 +145,3 @@ export function rafThrottle(fn: (args: Record<string, unknown>) => unknown): (..
|
||||
}
|
||||
|
||||
export const objToArray = castArray
|
||||
|
||||
export const isVNode = (node: unknown): boolean => {
|
||||
return node !== null && typeof node === 'object' && hasOwn(node, 'componentOptions')
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user