| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- import { onBeforeUnmount, onMounted, ref } from 'vue'
- import { FOCUSOUT_PREVENTED, FOCUSOUT_PREVENTED_OPTS } from './tokens'
- const focusReason = ref<'pointer' | 'keyboard'>()
- const lastUserFocusTimestamp = ref<number>(0)
- const lastAutomatedFocusTimestamp = ref<number>(0)
- let focusReasonUserCount = 0
- 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 || node === document.activeElement
- ? 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 })
- lastAutomatedFocusTimestamp.value = window.performance.now()
- 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()
- export const isFocusCausedByUserEvent = (): boolean => {
- return lastUserFocusTimestamp.value > lastAutomatedFocusTimestamp.value
- }
- const notifyFocusReasonPointer = () => {
- focusReason.value = 'pointer'
- lastUserFocusTimestamp.value = window.performance.now()
- }
- const notifyFocusReasonKeydown = () => {
- focusReason.value = 'keyboard'
- lastUserFocusTimestamp.value = window.performance.now()
- }
- export const useFocusReason = (): {
- focusReason: typeof focusReason
- lastUserFocusTimestamp: typeof lastUserFocusTimestamp
- lastAutomatedFocusTimestamp: typeof lastAutomatedFocusTimestamp
- } => {
- onMounted(() => {
- if (focusReasonUserCount === 0) {
- document.addEventListener('mousedown', notifyFocusReasonPointer)
- document.addEventListener('touchstart', notifyFocusReasonPointer)
- document.addEventListener('keydown', notifyFocusReasonKeydown)
- }
- focusReasonUserCount++
- })
- onBeforeUnmount(() => {
- focusReasonUserCount--
- if (focusReasonUserCount <= 0) {
- document.removeEventListener('mousedown', notifyFocusReasonPointer)
- document.removeEventListener('touchstart', notifyFocusReasonPointer)
- document.removeEventListener('keydown', notifyFocusReasonKeydown)
- }
- })
- return {
- focusReason,
- lastUserFocusTimestamp,
- lastAutomatedFocusTimestamp,
- }
- }
- export const createFocusOutPreventedEvent = (
- detail: CustomEventInit['detail']
- ) => {
- return new CustomEvent(FOCUSOUT_PREVENTED, {
- ...FOCUSOUT_PREVENTED_OPTS,
- detail,
- })
- }
|