Files
holesail-browser/extension/dashboard/ui/modal.js
T
Raven Scott 6fcd9fcf2b
CI / Build & Test (push) Successful in 3m19s
eat: implement Holesail-Browser enhancement plan
- Add unit tests (hostname-validator, TLDs, payload-schemas) and integration tests for message handler registry
- Refactor native host message router into handler registry (handlers/state, tunnels, ssh, rdp, backup, ca, connections)
- Add ESLint config and npm test + lint steps in CI
- Dashboard: visibility-based refresh pause, configurable refresh interval (2s/5s/10s/paused)
- Accessibility: ARIA on nav and modals, focus trap and restore, prefers-reduced-motion
- Empty states: primary action buttons for virtual hosts, servers, service tunnels
- Native host rate limiting for backup and CA operations; update SECURITY.md
- CONTRIBUTING: "Adding a new dashboard page", dev workflow; add npm run dev script
2026-03-15 00:24:31 -04:00

102 lines
3.4 KiB
JavaScript

/**
* Generic modal helpers for the dashboard.
* Handles open/close animations, error display, backdrop click, and Escape key.
* Depends on: core/utils.js ($), core/navigation.js (navigateTo)
*/
let _modalPreviousFocus = null;
/**
* Open a modal by adding the `open` class and focusing the first focusable element.
* Stores the previously focused element for restore on close. Sets ARIA attributes.
* @param {string} id - The modal backdrop element ID.
*/
function openModal(id) {
const el = $(id);
if (!el) return;
_modalPreviousFocus = document.activeElement;
el.classList.add('open');
el.setAttribute('aria-modal', 'true');
el.setAttribute('role', 'dialog');
const modalTitle = el.querySelector('.modal-title');
if (modalTitle && modalTitle.id) el.setAttribute('aria-labelledby', modalTitle.id);
else if (modalTitle && !modalTitle.id) {
const tid = 'modal-title-' + id;
modalTitle.id = tid;
el.setAttribute('aria-labelledby', tid);
}
setTimeout(() => {
const focusable = el.querySelector('input:not([type="checkbox"]):not([disabled]), button:not([disabled]), [tabindex="0"]');
if (focusable) focusable.focus();
}, 50);
}
/**
* Close a modal by removing the `open` class and clearing any error messages.
* Restores focus to the element that had focus before the modal opened.
* @param {string} id - The modal backdrop element ID.
*/
function closeModal(id) {
const el = $(id);
if (!el) return;
el.classList.remove('open');
el.removeAttribute('aria-modal');
el.removeAttribute('role');
el.removeAttribute('aria-labelledby');
el.querySelectorAll('.modal-error').forEach(e => { e.style.display = 'none'; e.textContent = ''; });
if (_modalPreviousFocus && typeof _modalPreviousFocus.focus === 'function') {
_modalPreviousFocus.focus();
_modalPreviousFocus = null;
}
}
/**
* Display an error message inside a modal's error element.
* @param {string} modalId - Unused; kept for API symmetry.
* @param {string} errorId - ID of the error display element.
* @param {string} msg - Error message text.
*/
function showModalError(modalId, errorId, msg) {
const el = $(errorId);
if (!el) return;
el.textContent = msg;
el.style.display = 'block';
}
// Close modals on backdrop click or close button
document.addEventListener('click', (e) => {
const closeBtn = e.target.closest('[data-close-modal]');
if (closeBtn) {
closeModal(closeBtn.dataset.closeModal);
return;
}
if (e.target.classList.contains('modal-backdrop')) {
closeModal(e.target.id);
}
const pageLink = e.target.closest('[data-page-link]');
if (pageLink) {
navigateTo(pageLink.dataset.pageLink);
}
});
// Escape key closes topmost open modal; Tab trap focus inside modal
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const open = document.querySelector('.modal-backdrop.open');
if (open) closeModal(open.id);
return;
}
if (e.key !== 'Tab') return;
const open = document.querySelector('.modal-backdrop.open');
if (!open) return;
const focusable = open.querySelectorAll('input:not([disabled]), button:not([disabled]), select:not([disabled]), [tabindex="0"]');
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (!first || !last) return;
if (e.shiftKey) {
if (document.activeElement === first) { e.preventDefault(); last.focus(); }
} else {
if (document.activeElement === last) { e.preventDefault(); first.focus(); }
}
});