stackchain-dashboard/frontend/mobile-search-modal.js
timmy 4855bc7c8d
All checks were successful
CI / lint (pull_request) Successful in 3m42s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 6m32s
CI / release-candidate (pull_request) Has been skipped
feat: contain mobile Search focus through Preview (Closes #1390)
2026-08-25 10:08:55 +00:00

84 lines
3.0 KiB
JavaScript

(function (root, factory) {
const createMobileSearchModal = factory();
if (typeof module !== 'undefined' && module.exports) module.exports = createMobileSearchModal;
else root.createMobileSearchModal = createMobileSearchModal;
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
const FOCUSABLE = [
'a[href]', 'button', 'input', 'select', 'textarea',
'[tabindex]:not([tabindex="-1"])', '[contenteditable="true"]',
].join(',');
return function createMobileSearchModal(options) {
const documentRef = options.document;
const backgrounds = Array.from(options.backgrounds || [
documentRef.querySelector?.('body > header'),
documentRef.querySelector?.('main'),
documentRef.querySelector?.('#mobile-task-dock'),
]).filter(Boolean);
let surface = null;
let opener = null;
let priorInert = null;
function focusable() {
if (!surface || typeof surface.querySelectorAll !== 'function') return [];
return Array.from(surface.querySelectorAll(FOCUSABLE)).filter(element =>
!element.disabled && !element.hidden && element.getAttribute?.('aria-hidden') !== 'true' &&
(typeof element.matches !== 'function' || element.matches(':not([hidden])')) &&
(typeof element.getClientRects !== 'function' || element.getClientRects().length > 0)
);
}
function focus(element) {
if (element?.isConnected !== false && typeof element?.focus === 'function') element.focus();
}
function onKeydown(event) {
if (!surface || event.key !== 'Tab') return;
const controls = focusable();
if (!controls.length) {
event.preventDefault();
return;
}
const first = controls[0];
const last = controls[controls.length - 1];
if (event.shiftKey && documentRef.activeElement === first) {
event.preventDefault();
focus(last);
} else if (!event.shiftKey && documentRef.activeElement === last) {
event.preventDefault();
focus(first);
} else if (!controls.includes(documentRef.activeElement)) {
event.preventDefault();
focus(event.shiftKey ? last : first);
}
}
function transition(nextSurface, transitionOptions = {}) {
surface = nextSurface;
focus(transitionOptions.initialFocus || focusable()[0]);
}
function activate(nextSurface, activateOptions = {}) {
if (!surface) {
opener = activateOptions.opener || documentRef.activeElement || null;
priorInert = backgrounds.map(element => element.inert === true);
backgrounds.forEach(element => { element.inert = true; });
}
transition(nextSurface, activateOptions);
}
function deactivate() {
if (!surface) return;
backgrounds.forEach((element, index) => { element.inert = priorInert[index]; });
surface = null;
priorInert = null;
const restore = opener;
opener = null;
focus(restore);
}
documentRef.addEventListener('keydown', onKeydown);
return { activate, transition, deactivate, current: () => surface };
};
});