flyonui
Version:
The easiest, free and open-source Tailwind CSS component library with semantic classes.
200 lines (166 loc) • 5.76 kB
text/typescript
import { IAccessibilityComponent, IAccessibilityKeyboardHandlers } from './interfaces'
class HSAccessibilityObserver {
private components: IAccessibilityComponent[] = []
private currentlyOpenedComponents: IAccessibilityComponent[] = []
private activeComponent: IAccessibilityComponent | null = null
constructor() {
this.initGlobalListeners()
}
private initGlobalListeners(): void {
document.addEventListener('keydown', evt => this.handleGlobalKeydown(evt))
document.addEventListener('focusin', evt => this.handleGlobalFocusin(evt))
}
private getActiveComponent(el: HTMLElement) {
if (!el) return null
const containingComponents = this.components.filter(
comp => comp.wrapper.contains(el) || (comp.context && comp.context.contains(el))
)
if (containingComponents.length === 0) return null
if (containingComponents.length === 1) return containingComponents[0]
let closestComponent = null
let minDistance = Number.MAX_SAFE_INTEGER
for (const comp of containingComponents) {
let distance = 0
let current = el
while (current && current !== comp.wrapper && current !== comp.context) {
distance++
current = current.parentElement
}
if (distance < minDistance) {
minDistance = distance
closestComponent = comp
}
}
return closestComponent
}
private handleGlobalFocusin(evt: FocusEvent): void {
const target = evt.target as HTMLElement
this.activeComponent = this.getActiveComponent(target)
}
private handleGlobalKeydown(evt: KeyboardEvent): void {
const target = evt.target as HTMLElement
this.activeComponent = this.getActiveComponent(target)
if (!this.activeComponent) return
switch (evt.key) {
case 'Escape':
if (!this.activeComponent.isOpened) {
const closestOpenParent = this.findClosestOpenParent(target)
if (closestOpenParent?.handlers.onEsc) {
closestOpenParent.handlers.onEsc()
evt.preventDefault()
evt.stopPropagation()
}
} else if (this.activeComponent.handlers.onEsc) {
this.activeComponent.handlers.onEsc()
evt.preventDefault()
evt.stopPropagation()
}
break
case 'Enter':
if (this.activeComponent.handlers.onEnter) {
this.activeComponent.handlers.onEnter()
evt.preventDefault()
evt.stopPropagation()
}
break
case ' ':
case 'Space':
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
return
}
if (this.activeComponent.handlers.onSpace) {
this.activeComponent.handlers.onSpace()
evt.preventDefault()
evt.stopPropagation()
}
break
case 'ArrowDown':
case 'ArrowUp':
case 'ArrowLeft':
case 'ArrowRight':
if (this.activeComponent.handlers.onArrow) {
if (evt.metaKey || evt.ctrlKey || evt.altKey || evt.shiftKey) {
return
}
this.activeComponent.handlers.onArrow(evt)
evt.preventDefault()
evt.stopPropagation()
}
break
case 'Tab':
if (!this.activeComponent.handlers.onTab) break
const handler = evt.shiftKey ? this.activeComponent.handlers.onShiftTab : this.activeComponent.handlers.onTab
if (handler) handler()
break
case 'Home':
if (this.activeComponent.handlers.onHome) {
this.activeComponent.handlers.onHome()
evt.preventDefault()
evt.stopPropagation()
}
break
case 'End':
if (this.activeComponent.handlers.onEnd) {
this.activeComponent.handlers.onEnd()
evt.preventDefault()
evt.stopPropagation()
}
break
default:
if (this.activeComponent.handlers.onFirstLetter && evt.key.length === 1 && /^[a-zA-Z]$/.test(evt.key)) {
this.activeComponent.handlers.onFirstLetter(evt.key)
evt.preventDefault()
evt.stopPropagation()
}
break
}
}
private findClosestOpenParent(target: HTMLElement): IAccessibilityComponent | null {
let current = target.parentElement
while (current) {
const parentComponent = this.currentlyOpenedComponents.find(
comp => comp.wrapper === current && comp !== this.activeComponent
)
if (parentComponent) {
return parentComponent
}
current = current.parentElement
}
return null
}
public registerComponent(
wrapper: HTMLElement,
handlers: IAccessibilityKeyboardHandlers,
isOpened: boolean = true,
name: string = '',
selector: string = '',
context?: HTMLElement
): IAccessibilityComponent {
const component: IAccessibilityComponent = {
wrapper,
handlers,
isOpened,
name,
selector,
context,
isRegistered: true
}
this.components.push(component)
return component
}
public updateComponentState(component: IAccessibilityComponent, isOpened: boolean): void {
component.isOpened = isOpened
if (isOpened) {
if (!this.currentlyOpenedComponents.includes(component)) {
this.currentlyOpenedComponents.push(component)
}
} else {
this.currentlyOpenedComponents = this.currentlyOpenedComponents.filter(comp => comp !== component)
}
}
public unregisterComponent(component: IAccessibilityComponent): void {
this.components = this.components.filter(comp => comp !== component)
this.currentlyOpenedComponents = this.currentlyOpenedComponents.filter(comp => comp !== component)
}
}
export default HSAccessibilityObserver