(function (root, factory) { "use strict"; const api = factory(); if (typeof module === "object" && module.exports) { module.exports = api; } else { root.LegacyModalFocus = api; } }(typeof globalThis !== "undefined" ? globalThis : this, function () { "use strict"; const defaultModalSelector = "[role='dialog'], [role='alertdialog']"; const focusableSelector = [ "a[href]", "area[href]", "button", "input", "select", "textarea", "iframe", "object", "embed", "summary", "audio[controls]", "video[controls]", "[contenteditable]", "[tabindex]" ].join(","); function attribute(element, name) { return element && typeof element.getAttribute === "function" ? element.getAttribute(name) : null; } function hasAttribute(element, name) { return !!element && typeof element.hasAttribute === "function" && element.hasAttribute(name); } function computedStyle(element, documentObject) { const view = documentObject && documentObject.defaultView; if (!view || typeof view.getComputedStyle !== "function") return null; try { return view.getComputedStyle(element); } catch (_) { return null; } } function isVisible(element, documentObject) { if (!element) return false; const documentRef = documentObject || element.ownerDocument; if (documentRef && typeof documentRef.contains === "function" && !documentRef.contains(element)) return false; for (let current = element; current; current = current.parentElement) { if (current.hidden === true || current.inert === true || hasAttribute(current, "hidden") || hasAttribute(current, "inert") || attribute(current, "aria-hidden") === "true") return false; const style = computedStyle(current, documentRef); if (style && (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || style.contentVisibility === "hidden")) { return false; } } return true; } function isNativeFocusable(element) { const tagName = String(element && element.tagName || "").toLowerCase(); if (["button", "select", "textarea", "iframe", "object", "embed", "summary"] .includes(tagName)) return true; if (tagName === "input") return String(attribute(element, "type") || "").toLowerCase() !== "hidden"; if (tagName === "a" || tagName === "area") return hasAttribute(element, "href"); if (tagName === "audio" || tagName === "video") return hasAttribute(element, "controls"); return hasAttribute(element, "contenteditable") && attribute(element, "contenteditable") !== "false"; } function isFocusable(element, documentObject) { if (!element || typeof element.focus !== "function" || !isVisible(element, documentObject)) return false; if (element.disabled === true || hasAttribute(element, "disabled") || attribute(element, "aria-disabled") === "true") return false; if (typeof element.matches === "function") { try { if (element.matches(":disabled")) return false; } catch (_) { // Minimal WebView test doubles do not have to implement selector matching. } } const tabIndexAttribute = attribute(element, "tabindex"); if (tabIndexAttribute !== null && Number(tabIndexAttribute) < 0) return false; return isNativeFocusable(element) || (tabIndexAttribute !== null && Number(tabIndexAttribute) >= 0); } function getFocusableElements(modal, documentObject) { if (!modal || typeof modal.querySelectorAll !== "function") return []; return Array.from(modal.querySelectorAll(focusableSelector)).filter(function (element) { return isFocusable(element, documentObject || modal.ownerDocument); }); } function findVisibleModals(documentObject, selector) { if (!documentObject || typeof documentObject.querySelectorAll !== "function") return []; return Array.from(documentObject.querySelectorAll(selector || defaultModalSelector)) .filter(function (modal) { return isVisible(modal, documentObject); }); } function findVisibleModal(documentObject, selector) { const modals = findVisibleModals(documentObject, selector); return modals.length > 0 ? modals[modals.length - 1] : null; } function contains(container, element) { return !!container && !!element && typeof container.contains === "function" && container.contains(element); } function focus(element) { if (!element || typeof element.focus !== "function") return false; try { element.focus({ preventScroll: true }); } catch (_) { try { element.focus(); } catch (_) { return false; } } return true; } function resolveFocusTarget(target) { let resolved = target; const visited = new Set(); while (typeof resolved === "function") { if (visited.has(resolved)) return null; visited.add(resolved); try { resolved = resolved(); } catch (_) { return null; } } return resolved || null; } function preferredElement(modal, preferred, documentObject) { let candidate = preferred; if (typeof candidate === "function") candidate = candidate(modal); if (typeof candidate === "string" && typeof modal.querySelector === "function") { candidate = modal.querySelector(candidate); } if (isFocusable(candidate, documentObject) && contains(modal, candidate)) return candidate; if (typeof modal.querySelector === "function") { candidate = modal.querySelector("[data-modal-initial-focus], [autofocus]"); if (isFocusable(candidate, documentObject) && contains(modal, candidate)) return candidate; } const focusable = getFocusableElements(modal, documentObject); return focusable.length > 0 ? focusable[0] : null; } function create(options) { const settings = options || {}; const documentObject = settings.document || (typeof document !== "undefined" ? document : null); if (!documentObject) throw new Error("LegacyModalFocus requires a document."); const selector = settings.modalSelector || defaultModalSelector; const entries = new Map(); let activeModal = null; let observer = null; let started = false; function entryFor(modal, opener) { let entry = entries.get(modal); if (!entry) { entry = { opener: opener || null, lastFocused: null }; entries.set(modal, entry); } return entry; } function rememberOpener(modal, opener) { if (!modal) return null; const entry = entryFor(modal, opener || documentObject.activeElement); if (!entry.opener) entry.opener = opener || documentObject.activeElement || null; return entry.opener; } function modalContaining(element) { for (const modal of entries.keys()) { if (contains(modal, element)) return modal; } return null; } function unwindReturnTarget(entry, visibleSet) { let target = resolveFocusTarget(entry && entry.opener); const visited = new Set(); while (target) { const owner = modalContaining(target); if (!owner || visibleSet.has(owner)) break; if (visited.has(owner)) return null; visited.add(owner); const ownerEntry = entries.get(owner); target = resolveFocusTarget(ownerEntry && ownerEntry.opener); } return isFocusable(target, documentObject) ? target : null; } function cleanClosedEntries(visibleSet) { for (const modal of Array.from(entries.keys())) { if (!visibleSet.has(modal)) entries.delete(modal); } } function sync(preferredFocus) { const visible = findVisibleModals(documentObject, selector); const visibleSet = new Set(visible); const nextModal = visible.length > 0 ? visible[visible.length - 1] : null; const previousModal = activeModal; const previousEntry = previousModal && entries.get(previousModal); if (previousModal && previousEntry && contains(previousModal, documentObject.activeElement)) { previousEntry.lastFocused = documentObject.activeElement; } if (nextModal !== previousModal) { if (nextModal) { const opener = documentObject.activeElement; const nextEntry = entryFor(nextModal, contains(nextModal, opener) ? null : opener); activeModal = nextModal; if (previousModal && !visibleSet.has(previousModal)) { const returned = unwindReturnTarget(previousEntry, visibleSet); if (returned && contains(nextModal, returned)) { nextEntry.lastFocused = returned; focus(returned); } else { focus(nextEntry.lastFocused && isFocusable(nextEntry.lastFocused, documentObject) ? nextEntry.lastFocused : preferredElement(nextModal, preferredFocus || settings.preferredFocus, documentObject)); } } else { focus(preferredElement(nextModal, preferredFocus || settings.preferredFocus, documentObject)); } } else { activeModal = null; focus(unwindReturnTarget(previousEntry, visibleSet)); } } else if (nextModal && preferredFocus && !contains(nextModal, documentObject.activeElement)) { focus(preferredElement(nextModal, preferredFocus, documentObject)); } cleanClosedEntries(visibleSet); return activeModal; } function onFocusIn(event) { const modal = activeModal || findVisibleModal(documentObject, selector); if (!modal || !contains(modal, event.target)) return; entryFor(modal, null).lastFocused = event.target; } function handleKeydown(event) { if (!event || event.defaultPrevented || event.key !== "Tab") return false; const modal = activeModal || sync(); if (!modal) return false; const focusable = getFocusableElements(modal, documentObject); if (focusable.length === 0) { if (typeof event.preventDefault === "function") event.preventDefault(); if (!hasAttribute(modal, "tabindex") && typeof modal.setAttribute === "function") { modal.setAttribute("tabindex", "-1"); } focus(modal); return true; } const current = documentObject.activeElement; const index = focusable.indexOf(current); let target = null; if (event.shiftKey === true && index <= 0) { target = focusable[focusable.length - 1]; } else if (event.shiftKey !== true && (index < 0 || index === focusable.length - 1)) { target = focusable[0]; } if (!target) return false; if (typeof event.preventDefault === "function") event.preventDefault(); focus(target); return true; } function start() { if (started) return manager; started = true; documentObject.addEventListener("keydown", handleKeydown, true); documentObject.addEventListener("focusin", onFocusIn, true); const Observer = settings.MutationObserver || (documentObject.defaultView && documentObject.defaultView.MutationObserver); if (typeof Observer === "function") { observer = new Observer(function () { sync(); }); observer.observe(documentObject.body || documentObject.documentElement, { attributes: true, attributeFilter: ["hidden", "aria-hidden", "inert", "style", "class"], childList: true, subtree: true }); } sync(); return manager; } function stop() { if (!started) return; started = false; documentObject.removeEventListener("keydown", handleKeydown, true); documentObject.removeEventListener("focusin", onFocusIn, true); if (observer) observer.disconnect(); observer = null; } const manager = { getActiveModal: function () { return activeModal; }, getFocusableElements: function (modal) { return getFocusableElements(modal || activeModal, documentObject); }, handleKeydown: handleKeydown, rememberOpener: rememberOpener, start: start, stop: stop, sync: sync }; return manager; } return { create: create, findVisibleModal: findVisibleModal, findVisibleModals: findVisibleModals, getFocusableElements: getFocusableElements, isFocusable: isFocusable, isVisible: isVisible }; }));