UNPKG

smdm-slide-menu

Version:

A library agnostic multilevel page menu with a smooth slide effect based on CSS transitions, focused on accessibility.

131 lines (108 loc) 3.61 kB
export function wrapElement(elem: HTMLElement, wrapper: HTMLElement): HTMLElement { if (elem.parentElement === null) { throw Error('`elem` has no parentElement'); } elem.parentElement.insertBefore(wrapper, elem); wrapper.appendChild(elem); return elem; } export function unwrapElement(elem: HTMLElement): void { const parent = elem.parentElement; if (parent === null) { throw Error('`elem` has no parentElement'); } while (elem.firstChild) { parent.insertBefore(elem.firstChild, elem); } parent.removeChild(elem); } export function parents(elem: Node | null, selector: string, limit?: number): HTMLElement[] { const matched: HTMLElement[] = []; while ( elem && elem.parentElement !== null && (limit === undefined ? true : matched.length < limit) ) { if (elem instanceof HTMLElement && elem.matches(selector)) { matched.push(elem); } elem = elem.parentElement; } return matched; } export function parentsOne(elem: Node, selector: string): HTMLElement | null { const matches = parents(elem, selector, 1); return matches.length ? matches[0] : null; } export function getDistanceFromTop(element: Element) { if (!element) { throw new Error('Element is not defined'); } const rect = element.getBoundingClientRect(); const distance = rect.top; return distance; } export const TAB_ABLE_SELECTOR = 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'; export function focusFirstTabAbleElemIn(elem: HTMLElement | null | undefined): void { const firstTabbaleElem = Array.from(elem?.querySelectorAll(TAB_ABLE_SELECTOR) ?? []).find( (elem) => { return isVisible(elem); }, ); // @ts-expect-error // possibly undefined element firstTabbaleElem?.focus(); } function isVisible(element: Element) { // @ts-expect-error // stop checking when reaching document for (let el = element; el && el !== document; el = el.parentNode) { // If current element has display property 'none', return false // @ts-expect-error // accessing style of Element if (el.style?.display === 'none' || getComputedStyle(el).display === 'none') { return false; } } return true; } export function trapFocus( event: KeyboardEvent, targetElement: HTMLElement, firstElement?: HTMLElement, lastElement?: HTMLElement, ) { const focusableElements = targetElement.querySelectorAll(TAB_ABLE_SELECTOR); const firstFocusableElement = firstElement ?? focusableElements[0]; const lastFocusableElement = lastElement ?? focusableElements[focusableElements.length - 1]; const KEYCODE_TAB = 9; const isTabPressed = event.key === 'Tab' || event.keyCode === KEYCODE_TAB; if (!isTabPressed) { return; } if (event.shiftKey) { /* shift + tab */ if (document.activeElement === firstFocusableElement) { // @ts-ignore lastFocusableElement.focus(); event.preventDefault(); } } /* tab */ else { if (document.activeElement === lastFocusableElement) { // @ts-ignore firstFocusableElement.focus(); event.preventDefault(); } } } export function alignTop(elem: HTMLElement): void { const dy = getDistanceFromTop(elem); if (dy > 0) { elem.style.top = `-${dy}px`; } } export function validateQuery(str: string): boolean { try { document.querySelector(str); return true; } catch (e) { return false; } }