diff --git a/admin/index.js b/admin/index.js index 983c799..6526ba9 100644 --- a/admin/index.js +++ b/admin/index.js @@ -42,3 +42,14 @@ document.addEventListener('submit', (e) => { e.preventDefault(); } }); + +// Announcement editor dialog: the server flags it for ?new / ?edit URLs. +// Closing it any way (× link, Esc) drops the query string, so a reload +// doesn't reopen it. +const annModal = document.getElementById('modal-ann'); +if (annModal?.hasAttribute('data-open-on-load')) { + annModal.showModal(); + annModal.addEventListener('close', () => { + history.replaceState(null, '', 'index.php'); + }); +} diff --git a/admin/index.php b/admin/index.php index 12459c4..90546f3 100644 --- a/admin/index.php +++ b/admin/index.php @@ -239,11 +239,10 @@ $announcements = get_all_announcements(); - - + diff --git a/admin/menu.js b/admin/menu.js index 5b81142..d12cacb 100644 --- a/admin/menu.js +++ b/admin/menu.js @@ -5,8 +5,8 @@ const csrfToken = document.body.dataset.csrfToken; // Item Modal helpers -const itemModal = document.getElementById('modal-item-backdrop'); -const catModal = document.getElementById('modal-cat-backdrop'); +const itemModal = document.getElementById('modal-item'); +const catModal = document.getElementById('modal-cat'); function openItemModal(itemData = null) { const title = document.getElementById('modal-item-title'); @@ -37,12 +37,12 @@ function openItemModal(itemData = null) { availField.checked = true; } - itemModal.classList.add('is-open'); + itemModal.showModal(); nameField.focus(); } function closeItemModal() { - itemModal.classList.remove('is-open'); + itemModal.close(); } function openCategoryModal(catData = null) { @@ -63,12 +63,12 @@ function openCategoryModal(catData = null) { descField.value = ''; } - catModal.classList.add('is-open'); + catModal.showModal(); nameField.focus(); } function closeCategoryModal() { - catModal.classList.remove('is-open'); + catModal.close(); } function escapeHtml(text) { diff --git a/admin/menu.php b/admin/menu.php index c676209..9ec0fac 100644 --- a/admin/menu.php +++ b/admin/menu.php @@ -328,9 +328,8 @@ $menuEnabled = is_menu_enabled(); - - + - - + diff --git a/admin/users.js b/admin/users.js index 2514689..3320ca4 100644 --- a/admin/users.js +++ b/admin/users.js @@ -5,9 +5,9 @@ const csrfToken = document.body.dataset.csrfToken; const currentUserId = document.body.dataset.currentUserId; -const userModal = document.getElementById('modal-user-backdrop'); -const passwordModal = document.getElementById('modal-password-backdrop'); -const editModal = document.getElementById('modal-edit-backdrop'); +const userModal = document.getElementById('modal-user'); +const passwordModal = document.getElementById('modal-password'); +const editModal = document.getElementById('modal-edit'); function escapeHtml(text) { const div = document.createElement('div'); @@ -54,16 +54,14 @@ function openUserModal() { const form = document.getElementById('form-user'); form.reset(); resetPasswordVisibility(form); - userModal.classList.add('is-open'); + userModal.showModal(); } -// Safari ignores autocomplete="off" on fields labeled "Username" and offers -// saved logins whenever one has focus. The modals used to auto-focus that -// field, and a closed modal left it focused, so the prompt kept popping up. -// Now nothing is auto-focused, and closing a modal drops focus. +// Native : close() hides it (not rendered, so its fields can't keep +// focus or trigger autofill) and returns focus to whatever opened it. +// Esc closes it too, with no extra code. function closeModal(modal) { - modal.classList.remove('is-open'); - if (modal.contains(document.activeElement)) document.activeElement.blur(); + modal.close(); } function closeUserModal() { @@ -84,7 +82,7 @@ function openPasswordModal(userId, username) { document.getElementById('password-current-group').style.display = isSelf ? '' : 'none'; - passwordModal.classList.add('is-open'); + passwordModal.showModal(); (isSelf ? document.getElementById('password-field-current') : document.getElementById('password-field-new')).focus(); } @@ -105,7 +103,7 @@ function openEditModal(userId, username, role) { roleSelect.disabled = isSelf; document.getElementById('edit-role-self-hint').hidden = !isSelf; - editModal.classList.add('is-open'); + editModal.showModal(); } function closeEditModal() { diff --git a/admin/users.php b/admin/users.php index f9b0239..c5871ff 100644 --- a/admin/users.php +++ b/admin/users.php @@ -302,9 +302,8 @@ $users = get_all_users(); - - + - - + - - + diff --git a/src/scss/components/_admin-modal.scss b/src/scss/components/_admin-modal.scss index 35634e8..dfbc89c 100644 --- a/src/scss/components/_admin-modal.scss +++ b/src/scss/components/_admin-modal.scss @@ -1,49 +1,62 @@ /* ---------- admin modal dialogs ---------- */ -.admin-modal-backdrop { - position: fixed; - inset: 0; - background: rgba(15, 12, 10, 0.8); - backdrop-filter: blur(0.25rem); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: 1.5rem; - opacity: 0; - pointer-events: none; - // Closed modals must be visibility: hidden, not just transparent — - // otherwise their fields stay focusable and Safari offers saved logins - // for an invisible Username field. The visibility switch waits for the - // fade-out to finish so the close animation still plays. - visibility: hidden; - transition: opacity .2s ease, visibility 0s linear .2s; -} - -.admin-modal-backdrop.is-open { - opacity: 1; - pointer-events: auto; - visibility: visible; - transition: opacity .2s ease, visibility 0s; -} - +/* Native modals. The browser handles the overlay (::backdrop), + focus trapping, inertness of the page behind, and Esc. A closed dialog + is display: none, so its fields can't hold focus or trigger autofill. + Open/close fades rely on @starting-style for the entry and + allow-discrete to keep display and overlay alive through the exit. */ .admin-modal { background: var(--surface); + color: inherit; border: 1px solid var(--rule); border-radius: 0.25rem; - width: 100%; + width: calc(100% - 3rem); max-width: 36rem; max-height: 90vh; + margin: auto; overflow-y: auto; padding: 2rem; box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.6); + opacity: 0; transform: translateY(0.75rem); - transition: transform .2s ease; + transition: + opacity .2s ease, + transform .2s ease, + overlay .2s ease allow-discrete, + display .2s ease allow-discrete; } -.admin-modal-backdrop.is-open .admin-modal { +.admin-modal[open] { + opacity: 1; transform: translateY(0); } +.admin-modal::backdrop { + background-color: rgba(15, 12, 10, 0); + backdrop-filter: blur(0); + transition: + background-color .2s ease, + backdrop-filter .2s ease, + overlay .2s ease allow-discrete, + display .2s ease allow-discrete; +} + +.admin-modal[open]::backdrop { + background-color: rgba(15, 12, 10, 0.8); + backdrop-filter: blur(0.25rem); +} + +@starting-style { + .admin-modal[open] { + opacity: 0; + transform: translateY(0.75rem); + } + + .admin-modal[open]::backdrop { + background-color: rgba(15, 12, 10, 0); + backdrop-filter: blur(0); + } +} + .modal-header { display: flex; justify-content: space-between; diff --git a/styles.css b/styles.css index f73ea4f..ba8050a 100644 --- a/styles.css +++ b/styles.css @@ -785,47 +785,54 @@ button.status-badge:disabled { } } /* ---------- admin modal dialogs ---------- */ -.admin-modal-backdrop { - position: fixed; - inset: 0; - background: rgba(15, 12, 10, 0.8); - backdrop-filter: blur(0.25rem); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: 1.5rem; - opacity: 0; - pointer-events: none; - visibility: hidden; - transition: opacity 0.2s ease, visibility 0s linear 0.2s; -} - -.admin-modal-backdrop.is-open { - opacity: 1; - pointer-events: auto; - visibility: visible; - transition: opacity 0.2s ease, visibility 0s; -} - +/* Native modals. The browser handles the overlay (::backdrop), + focus trapping, inertness of the page behind, and Esc. A closed dialog + is display: none, so its fields can't hold focus or trigger autofill. + Open/close fades rely on @starting-style for the entry and + allow-discrete to keep display and overlay alive through the exit. */ .admin-modal { background: var(--surface); + color: inherit; border: 1px solid var(--rule); border-radius: 0.25rem; - width: 100%; + width: calc(100% - 3rem); max-width: 36rem; max-height: 90vh; + margin: auto; overflow-y: auto; padding: 2rem; box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.6); + opacity: 0; transform: translateY(0.75rem); - transition: transform 0.2s ease; + transition: opacity 0.2s ease, transform 0.2s ease, overlay 0.2s ease allow-discrete, display 0.2s ease allow-discrete; } -.admin-modal-backdrop.is-open .admin-modal { +.admin-modal[open] { + opacity: 1; transform: translateY(0); } +.admin-modal::backdrop { + background-color: rgba(15, 12, 10, 0); + backdrop-filter: blur(0); + transition: background-color 0.2s ease, backdrop-filter 0.2s ease, overlay 0.2s ease allow-discrete, display 0.2s ease allow-discrete; +} + +.admin-modal[open]::backdrop { + background-color: rgba(15, 12, 10, 0.8); + backdrop-filter: blur(0.25rem); +} + +@starting-style { + .admin-modal[open] { + opacity: 0; + transform: translateY(0.75rem); + } + .admin-modal[open]::backdrop { + background-color: rgba(15, 12, 10, 0); + backdrop-filter: blur(0); + } +} .modal-header { display: flex; justify-content: space-between;