UNPKG

coveo-search-ui

Version:

Coveo JavaScript Search Framework

161 lines (141 loc) 4.52 kB
import { Defer } from '../../misc/Defer'; import { without, chain } from 'underscore'; import { $$, Dom } from '../../utils/Dom'; export interface FocusTrapOptions { focusableSelector: string; } export class FocusTrap { private focusInEvent: (e: FocusEvent) => void; private focusOutEvent: (e: FocusEvent) => void; private hiddenElements: HTMLElement[]; private enabled: boolean; private get focusableElements(): HTMLElement[] { return chain(Dom.nodeListToArray(this.container.querySelectorAll(this.options.focusableSelector))) .filter(element => $$(element).isVisible()) .sortBy(element => element.tabIndex) .value(); } constructor( private container: HTMLElement, private options: FocusTrapOptions = { focusableSelector: '[tabindex], button' } ) { this.hiddenElements = []; this.enable(); } public disable() { document.removeEventListener('focusin', this.focusInEvent); document.removeEventListener('focusout', this.focusOutEvent); this.showHiddenElements(); this.enabled = false; } private enable() { document.addEventListener('focusin', (this.focusInEvent = e => this.onFocusIn(e))); document.addEventListener('focusout', (this.focusOutEvent = e => this.onFocusOut(e))); this.hideAllExcept(this.container); this.enabled = true; } private showHiddenElements() { while (this.hiddenElements.length) { this.hiddenElements.pop().removeAttribute('aria-hidden'); } } private hideElement(element: HTMLElement) { if (element.getAttribute('aria-hidden')) { return; } this.hiddenElements.push(element); element.setAttribute('aria-hidden', `${true}`); } private hideSiblings(allowedElement: HTMLElement) { const parent = allowedElement.parentElement; if (parent) { without($$(parent).children(), allowedElement).forEach(elementToHide => { this.hideElement(elementToHide); }); } } private hideAllExcept(allowedElement: HTMLElement) { this.hideSiblings(allowedElement); const parent = allowedElement.parentElement; if (parent && parent !== document.body) { this.hideAllExcept(parent); } } private getFocusableSibling(element: HTMLElement, previous = false) { const elements = this.focusableElements; const currentIndex = elements.indexOf(element); if (currentIndex === -1) { return null; } return elements[(currentIndex + (previous ? -1 : 1) + elements.length) % elements.length]; } private focusSibling(element: HTMLElement, previous = false) { const sibling = this.getFocusableSibling(element, previous); if (sibling) { sibling.focus(); } } private focusFirstElement() { const elements = this.focusableElements; if (elements.length) { elements[0].focus(); } } private elementIsBefore(oldElement: HTMLElement, newElement: HTMLElement) { if (!newElement) { return false; } return oldElement.compareDocumentPosition(newElement) === Node.DOCUMENT_POSITION_PRECEDING; } private onLosingFocus(oldElement: HTMLElement, newElement: HTMLElement) { Defer.defer(() => { if (!this.enabled) { return; } this.enabled = false; if (oldElement && this.focusIsAllowed(oldElement)) { this.focusSibling(oldElement, this.elementIsBefore(oldElement, newElement)); } else { this.focusFirstElement(); } this.enabled = true; }); } private focusIsAllowed(element: HTMLElement) { return this.container.contains(element); } private elementIsInPage(element: HTMLElement) { return element && element !== document.body.parentElement; } private onFocusIn(e: FocusEvent) { if (!this.enabled) { return; } const oldElement = e.relatedTarget as HTMLElement; const handledByFocusOut = this.elementIsInPage(oldElement); if (handledByFocusOut) { return; } const newElement = e.target as HTMLElement; if (!this.elementIsInPage(newElement)) { return; } if (!this.focusIsAllowed(newElement)) { this.onLosingFocus(null, newElement); } } private onFocusOut(e: FocusEvent) { if (!this.enabled) { return; } const newElement = e.relatedTarget as HTMLElement; if (!this.elementIsInPage(newElement)) { return; } if (!newElement || !this.focusIsAllowed(newElement)) { this.onLosingFocus(e.target as HTMLElement, newElement); } } }