Refactor admin modals: replace backdrop divs with native <dialog> elements for improved accessibility and functionality
This commit is contained in:
1 parent
242c5165e1
commit
02551eebbc
8 files changed
+123
-106
No files matched your search
@@ -42,3 +42,14 @@ document.addEventListener('submit', (e) => {
|
|||||||
e.preventDefault();
|
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');
|
||||||
|
});
|
||||||
|
}
|
||||||
+5
-7
@@ -239,11 +239,10 @@ $announcements = get_all_announcements();
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Modal Backdrop: Announcement Editor (Create / Edit) — server-rendered
|
<!-- Modal: Announcement Editor (Create / Edit). The server marks it
|
||||||
open/closed via the is-open class, since this page has no JS file;
|
data-open-on-load for ?new / ?edit URLs and index.js opens it with
|
||||||
the underlying save is still a plain form POST + reload, unchanged. -->
|
showModal(); saving is still a plain form POST + reload. -->
|
||||||
<div class="admin-modal-backdrop<?= ($isCreatingNew || $editItem) ? ' is-open' : '' ?>" role="dialog" aria-modal="true" aria-labelledby="modal-ann-title">
|
<dialog class="admin-modal" id="modal-ann" aria-labelledby="modal-ann-title"<?= ($isCreatingNew || $editItem) ? ' data-open-on-load' : '' ?>>
|
||||||
<div class="admin-modal">
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="modal-ann-title"><?= $editItem ? '✏️ Edit Announcement' : '✨ New Announcement' ?></h2>
|
<h2 id="modal-ann-title"><?= $editItem ? '✏️ Edit Announcement' : '✨ New Announcement' ?></h2>
|
||||||
<a href="index.php" class="modal-close" aria-label="Close dialog">×</a>
|
<a href="index.php" class="modal-close" aria-label="Close dialog">×</a>
|
||||||
@@ -317,8 +316,7 @@ $announcements = get_all_announcements();
|
|||||||
<a href="index.php" class="btn btn-outline">Cancel</a>
|
<a href="index.php" class="btn btn-outline">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</dialog>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="index.js" defer></script>
|
<script src="index.js" defer></script>
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -5,8 +5,8 @@
|
|||||||
const csrfToken = document.body.dataset.csrfToken;
|
const csrfToken = document.body.dataset.csrfToken;
|
||||||
|
|
||||||
// Item Modal helpers
|
// Item Modal helpers
|
||||||
const itemModal = document.getElementById('modal-item-backdrop');
|
const itemModal = document.getElementById('modal-item');
|
||||||
const catModal = document.getElementById('modal-cat-backdrop');
|
const catModal = document.getElementById('modal-cat');
|
||||||
|
|
||||||
function openItemModal(itemData = null) {
|
function openItemModal(itemData = null) {
|
||||||
const title = document.getElementById('modal-item-title');
|
const title = document.getElementById('modal-item-title');
|
||||||
@@ -37,12 +37,12 @@ function openItemModal(itemData = null) {
|
|||||||
availField.checked = true;
|
availField.checked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
itemModal.classList.add('is-open');
|
itemModal.showModal();
|
||||||
nameField.focus();
|
nameField.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeItemModal() {
|
function closeItemModal() {
|
||||||
itemModal.classList.remove('is-open');
|
itemModal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCategoryModal(catData = null) {
|
function openCategoryModal(catData = null) {
|
||||||
@@ -63,12 +63,12 @@ function openCategoryModal(catData = null) {
|
|||||||
descField.value = '';
|
descField.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
catModal.classList.add('is-open');
|
catModal.showModal();
|
||||||
nameField.focus();
|
nameField.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeCategoryModal() {
|
function closeCategoryModal() {
|
||||||
catModal.classList.remove('is-open');
|
catModal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
|
|||||||
+6
-10
@@ -328,9 +328,8 @@ $menuEnabled = is_menu_enabled();
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Modal Backdrop: Menu Item -->
|
<!-- Modal: Menu Item -->
|
||||||
<div class="admin-modal-backdrop" id="modal-item-backdrop" role="dialog" aria-modal="true" aria-labelledby="modal-item-title">
|
<dialog class="admin-modal" id="modal-item" aria-labelledby="modal-item-title">
|
||||||
<div class="admin-modal">
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="modal-item-title">✨ Add Menu Item</h2>
|
<h2 id="modal-item-title">✨ Add Menu Item</h2>
|
||||||
<button type="button" class="modal-close" aria-label="Close dialog" id="btn-close-item-modal">×</button>
|
<button type="button" class="modal-close" aria-label="Close dialog" id="btn-close-item-modal">×</button>
|
||||||
@@ -383,12 +382,10 @@ $menuEnabled = is_menu_enabled();
|
|||||||
<button type="button" class="btn btn-outline" id="btn-cancel-item">Cancel</button>
|
<button type="button" class="btn btn-outline" id="btn-cancel-item">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</dialog>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal Backdrop: Category -->
|
<!-- Modal: Category -->
|
||||||
<div class="admin-modal-backdrop" id="modal-cat-backdrop" role="dialog" aria-modal="true" aria-labelledby="modal-cat-title">
|
<dialog class="admin-modal" id="modal-cat" aria-labelledby="modal-cat-title">
|
||||||
<div class="admin-modal">
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="modal-cat-title">✨ Add Category</h2>
|
<h2 id="modal-cat-title">✨ Add Category</h2>
|
||||||
<button type="button" class="modal-close" aria-label="Close dialog" id="btn-close-cat-modal">×</button>
|
<button type="button" class="modal-close" aria-label="Close dialog" id="btn-close-cat-modal">×</button>
|
||||||
@@ -415,8 +412,7 @@ $menuEnabled = is_menu_enabled();
|
|||||||
<button type="button" class="btn btn-outline" id="btn-cancel-cat">Cancel</button>
|
<button type="button" class="btn btn-outline" id="btn-cancel-cat">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</dialog>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="menu.js" defer></script>
|
<script src="menu.js" defer></script>
|
||||||
|
|
||||||
|
|||||||
+10
-12
@@ -5,9 +5,9 @@
|
|||||||
const csrfToken = document.body.dataset.csrfToken;
|
const csrfToken = document.body.dataset.csrfToken;
|
||||||
const currentUserId = document.body.dataset.currentUserId;
|
const currentUserId = document.body.dataset.currentUserId;
|
||||||
|
|
||||||
const userModal = document.getElementById('modal-user-backdrop');
|
const userModal = document.getElementById('modal-user');
|
||||||
const passwordModal = document.getElementById('modal-password-backdrop');
|
const passwordModal = document.getElementById('modal-password');
|
||||||
const editModal = document.getElementById('modal-edit-backdrop');
|
const editModal = document.getElementById('modal-edit');
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
@@ -54,16 +54,14 @@ function openUserModal() {
|
|||||||
const form = document.getElementById('form-user');
|
const form = document.getElementById('form-user');
|
||||||
form.reset();
|
form.reset();
|
||||||
resetPasswordVisibility(form);
|
resetPasswordVisibility(form);
|
||||||
userModal.classList.add('is-open');
|
userModal.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safari ignores autocomplete="off" on fields labeled "Username" and offers
|
// Native <dialog>: close() hides it (not rendered, so its fields can't keep
|
||||||
// saved logins whenever one has focus. The modals used to auto-focus that
|
// focus or trigger autofill) and returns focus to whatever opened it.
|
||||||
// field, and a closed modal left it focused, so the prompt kept popping up.
|
// Esc closes it too, with no extra code.
|
||||||
// Now nothing is auto-focused, and closing a modal drops focus.
|
|
||||||
function closeModal(modal) {
|
function closeModal(modal) {
|
||||||
modal.classList.remove('is-open');
|
modal.close();
|
||||||
if (modal.contains(document.activeElement)) document.activeElement.blur();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeUserModal() {
|
function closeUserModal() {
|
||||||
@@ -84,7 +82,7 @@ function openPasswordModal(userId, username) {
|
|||||||
|
|
||||||
document.getElementById('password-current-group').style.display = isSelf ? '' : 'none';
|
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();
|
(isSelf ? document.getElementById('password-field-current') : document.getElementById('password-field-new')).focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +103,7 @@ function openEditModal(userId, username, role) {
|
|||||||
roleSelect.disabled = isSelf;
|
roleSelect.disabled = isSelf;
|
||||||
document.getElementById('edit-role-self-hint').hidden = !isSelf;
|
document.getElementById('edit-role-self-hint').hidden = !isSelf;
|
||||||
|
|
||||||
editModal.classList.add('is-open');
|
editModal.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeEditModal() {
|
function closeEditModal() {
|
||||||
|
|||||||
+9
-15
@@ -302,9 +302,8 @@ $users = get_all_users();
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Modal Backdrop: Add User -->
|
<!-- Modal: Add User -->
|
||||||
<div class="admin-modal-backdrop" id="modal-user-backdrop" role="dialog" aria-modal="true" aria-labelledby="modal-user-title">
|
<dialog class="admin-modal" id="modal-user" aria-labelledby="modal-user-title">
|
||||||
<div class="admin-modal">
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="modal-user-title">✨ Add User</h2>
|
<h2 id="modal-user-title">✨ Add User</h2>
|
||||||
<button type="button" class="modal-close" aria-label="Close dialog" id="btn-close-user-modal">×</button>
|
<button type="button" class="modal-close" aria-label="Close dialog" id="btn-close-user-modal">×</button>
|
||||||
@@ -361,12 +360,10 @@ $users = get_all_users();
|
|||||||
<button type="button" class="btn btn-outline" id="btn-cancel-user">Cancel</button>
|
<button type="button" class="btn btn-outline" id="btn-cancel-user">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</dialog>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal Backdrop: Edit User -->
|
<!-- Modal: Edit User -->
|
||||||
<div class="admin-modal-backdrop" id="modal-edit-backdrop" role="dialog" aria-modal="true" aria-labelledby="modal-edit-title">
|
<dialog class="admin-modal" id="modal-edit" aria-labelledby="modal-edit-title">
|
||||||
<div class="admin-modal">
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="modal-edit-title">✏️ Edit User</h2>
|
<h2 id="modal-edit-title">✏️ Edit User</h2>
|
||||||
<button type="button" class="modal-close" aria-label="Close dialog" id="btn-close-edit-modal">×</button>
|
<button type="button" class="modal-close" aria-label="Close dialog" id="btn-close-edit-modal">×</button>
|
||||||
@@ -398,12 +395,10 @@ $users = get_all_users();
|
|||||||
<button type="button" class="btn btn-outline" id="btn-cancel-edit">Cancel</button>
|
<button type="button" class="btn btn-outline" id="btn-cancel-edit">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</dialog>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal Backdrop: Change Password -->
|
<!-- Modal: Change Password -->
|
||||||
<div class="admin-modal-backdrop" id="modal-password-backdrop" role="dialog" aria-modal="true" aria-labelledby="modal-password-title">
|
<dialog class="admin-modal" id="modal-password" aria-labelledby="modal-password-title">
|
||||||
<div class="admin-modal">
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="modal-password-title">🔑 Change Password</h2>
|
<h2 id="modal-password-title">🔑 Change Password</h2>
|
||||||
<button type="button" class="modal-close" aria-label="Close dialog" id="btn-close-password-modal">×</button>
|
<button type="button" class="modal-close" aria-label="Close dialog" id="btn-close-password-modal">×</button>
|
||||||
@@ -459,8 +454,7 @@ $users = get_all_users();
|
|||||||
<button type="button" class="btn btn-outline" id="btn-cancel-password">Cancel</button>
|
<button type="button" class="btn btn-outline" id="btn-cancel-password">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</dialog>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="users.js" defer></script>
|
<script src="users.js" defer></script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,62 @@
|
|||||||
/* ---------- admin modal dialogs ---------- */
|
/* ---------- admin modal dialogs ---------- */
|
||||||
.admin-modal-backdrop {
|
/* Native <dialog> modals. The browser handles the overlay (::backdrop),
|
||||||
position: fixed;
|
focus trapping, inertness of the page behind, and Esc. A closed dialog
|
||||||
inset: 0;
|
is display: none, so its fields can't hold focus or trigger autofill.
|
||||||
background: rgba(15, 12, 10, 0.8);
|
Open/close fades rely on @starting-style for the entry and
|
||||||
backdrop-filter: blur(0.25rem);
|
allow-discrete to keep display and overlay alive through the exit. */
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal {
|
.admin-modal {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
color: inherit;
|
||||||
border: 1px solid var(--rule);
|
border: 1px solid var(--rule);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
width: 100%;
|
width: calc(100% - 3rem);
|
||||||
max-width: 36rem;
|
max-width: 36rem;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
|
margin: auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.6);
|
box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.6);
|
||||||
|
opacity: 0;
|
||||||
transform: translateY(0.75rem);
|
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);
|
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 {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
+33
-26
@@ -785,47 +785,54 @@ button.status-badge:disabled {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ---------- admin modal dialogs ---------- */
|
/* ---------- admin modal dialogs ---------- */
|
||||||
.admin-modal-backdrop {
|
/* Native <dialog> modals. The browser handles the overlay (::backdrop),
|
||||||
position: fixed;
|
focus trapping, inertness of the page behind, and Esc. A closed dialog
|
||||||
inset: 0;
|
is display: none, so its fields can't hold focus or trigger autofill.
|
||||||
background: rgba(15, 12, 10, 0.8);
|
Open/close fades rely on @starting-style for the entry and
|
||||||
backdrop-filter: blur(0.25rem);
|
allow-discrete to keep display and overlay alive through the exit. */
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-modal {
|
.admin-modal {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
color: inherit;
|
||||||
border: 1px solid var(--rule);
|
border: 1px solid var(--rule);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
width: 100%;
|
width: calc(100% - 3rem);
|
||||||
max-width: 36rem;
|
max-width: 36rem;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
|
margin: auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.6);
|
box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.6);
|
||||||
|
opacity: 0;
|
||||||
transform: translateY(0.75rem);
|
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);
|
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 {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
Reference in new issue
Block a user