Files
element-plus/packages/components/focus-trap/src/utils.ts
jeremywu dd19cae2bc refactor(components): popper composables (#5035)
* refactor(components): popper composables

- Refactor popper composables

* updates

* updates for tooltip

* Updates for popper. TODO: fix controlled tooltip animation

* Fix controlled mode popper animation issue

* Add new feature for customizing tooltip theme

* Fix popover and popconfirm error

* - Add Collection component for wrapping a collection of component
- Add FocusTrap component for trap focus for popups
- Add RovingFocus component for roving focus component type
- Adjust dropdown component based on these newly added components
- Add popper-trigger component for placing the trigger
- TODO: Finish current dropdown component, and all component's tests plus documents

* Refactor popper

* Complete organizing popper

* Almost finish dropdown

* Update popper tests

* update only-child test

* Finish focus trap component test

* Finish tooltip content test

* Finish tooltip trigger tests

* Finish tooltip tests

* finish tests for Collection and RovingFocusGroup

* Fix test cases for timeselect & select & popover

* Fix popover, popconfirm, menu bug and test cases

* Fix select-v2 test error caused by updating popper

* Fix date-picker test issue for updating popper

* fix test cases

* Fix eslint

* Rebase dev & fix tests

* Remove unused code
2022-01-04 09:15:15 +08:00

135 lines
3.2 KiB
TypeScript

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
? 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 (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 && element.focus) {
const prevFocusedElement = document.activeElement
element.focus({ preventScroll: true })
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()