UNPKG

flyonui

Version:

The easiest, free and open-source Tailwind CSS component library with semantic classes.

200 lines (166 loc) 5.76 kB
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