diff --git a/frontend/auth/admin/AdminApp.vue b/frontend/auth/admin/AdminApp.vue index 6d8a42a..1479d31 100644 --- a/frontend/auth/admin/AdminApp.vue +++ b/frontend/auth/admin/AdminApp.vue @@ -36,6 +36,7 @@ const renameIdValue = ref('') const editingPermDisplay = ref(null) const renameDisplayValue = ref('') const dialog = ref({ type: null, data: null, busy: false, error: '' }) +const dialogPreviousFocus = ref(null) // Track element that had focus before dialog opened const safeIdRegex = /[^A-Za-z0-9:._~-]/g // Template refs for navigation @@ -404,8 +405,77 @@ async function toggleOrgPermission(org, permId, checked) { } } -function openDialog(type, data) { dialog.value = { type, data, busy: false, error: '' } } -function closeDialog() { dialog.value = { type: null, data: null, busy: false, error: '' } } +function openDialog(type, data) { + const focused = document.activeElement + dialogPreviousFocus.value = focused + + // For delete operations, store sibling info to help restore focus after deletion + if (type === 'confirm' && focused) { + const row = focused.closest('tr') + if (row) { + const tbody = row.closest('tbody') + if (tbody) { + const rows = Array.from(tbody.querySelectorAll('tr')) + const idx = rows.indexOf(row) + // Store context to find next/prev row after deletion + dialog.value.focusContext = { + tbody, + index: idx, + total: rows.length, + selector: 'button:not([disabled]), a' + } + } + } + } + + dialog.value = { ...dialog.value, type, data, busy: false, error: '' } +} + +function closeDialog() { + const prev = dialogPreviousFocus.value + const context = dialog.value.focusContext + dialog.value = { type: null, data: null, busy: false, error: '' } + // Restore focus after dialog closes + restoreFocusAfterDialog(prev, context) + dialogPreviousFocus.value = null +} + +/** + * Restore focus to the previously focused element, or find a sibling if deleted. + */ +function restoreFocusAfterDialog(prev, context) { + if (!prev) return + + // Check if the original element still exists in DOM and is focusable + if (document.body.contains(prev) && !prev.disabled) { + prev.focus() + return + } + + // Element was deleted - try to find a sibling using stored context + if (context?.tbody && context.selector) { + const rows = Array.from(context.tbody.querySelectorAll('tr')) + if (rows.length > 0) { + // Try the same index (next row moved up) or the last row + const targetIdx = Math.min(context.index, rows.length - 1) + const targetRow = rows[targetIdx] + const focusable = targetRow?.querySelector(context.selector) + if (focusable) { + focusable.focus() + return + } + } + } + + // Fallback: try to find any focusable element in the admin panels + const container = document.querySelector('.admin-panels') + if (!container) return + + const focusable = container.querySelector('button:not([disabled]), a, input:not([disabled]), [tabindex="0"]') + if (focusable) { + focusable.focus() + } +} // Keyboard navigation handlers function handleBreadcrumbKeydown(event) { diff --git a/frontend/src/components/HostProfileView.vue b/frontend/src/components/HostProfileView.vue index 3573d19..886176e 100644 --- a/frontend/src/components/HostProfileView.vue +++ b/frontend/src/components/HostProfileView.vue @@ -5,7 +5,7 @@

{{ subheading }}

-
+
-
+