UNPKG

preline

Version:

Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.

224 lines (190 loc) 6.01 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;