Implement keynav on admin views, bugfixes and better breadcrumb handling.

This commit is contained in:
Leo Vasanko
2025-12-10 15:13:31 +00:00
parent 048b4c6ff1
commit ce783cbf57
7 changed files with 736 additions and 56 deletions
+50 -1
View File
@@ -13,6 +13,7 @@ import AdminDialogs from '@/admin/AdminDialogs.vue'
import { useAuthStore } from '@/stores/auth'
import { getSettings, adminUiPath, makeUiHref } from '@/utils/settings'
import { apiJson } from '@/utils/api'
import { getDirection } from '@/utils/keynav'
const info = ref(null)
const loading = ref(true)
@@ -37,6 +38,15 @@ const renameDisplayValue = ref('')
const dialog = ref({ type: null, data: null, busy: false, error: '' })
const safeIdRegex = /[^A-Za-z0-9:._~-]/g
// Template refs for navigation
const breadcrumbsRef = ref(null)
const adminOverviewRef = ref(null)
const adminOrgDetailRef = ref(null)
const adminUserDetailRef = ref(null)
// Check if any modal/dialog is open (blocks arrow key navigation)
const hasActiveModal = computed(() => dialog.value.type !== null || showRegModal.value)
function sanitizeRenameId() { if (renameIdValue.value) renameIdValue.value = renameIdValue.value.replace(safeIdRegex, '') }
function handleGlobalClick(e) {
@@ -397,6 +407,36 @@ 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: '' } }
// Keyboard navigation handlers
function handleBreadcrumbKeydown(event) {
if (hasActiveModal.value) return
const direction = getDirection(event)
if (!direction) return
// Left/right handled internally by Breadcrumbs component
if (direction === 'down') {
event.preventDefault()
// Move to admin panel content
if (adminOverviewRef.value) {
adminOverviewRef.value.focusFirstElement?.()
} else if (adminOrgDetailRef.value) {
adminOrgDetailRef.value.focusFirstElement?.()
} else if (adminUserDetailRef.value) {
adminUserDetailRef.value.focusFirstElement?.()
}
}
}
function handlePanelNavigateOut(direction) {
if (hasActiveModal.value) return
if (direction === 'up') {
// Focus breadcrumbs - focus the current page's crumb
breadcrumbsRef.value?.focusCurrent?.()
}
}
async function refreshUserDetail() {
await loadOrgs()
if (selectedUser.value) {
@@ -483,7 +523,7 @@ async function submitDialog() {
<section v-else-if="authenticated && (info?.is_global_admin || info?.is_org_admin)" class="view-root view-root--wide view-admin">
<header class="view-header">
<h1>{{ pageHeading }}</h1>
<Breadcrumbs :entries="breadcrumbEntries" />
<Breadcrumbs ref="breadcrumbsRef" :entries="breadcrumbEntries" @keydown="handleBreadcrumbKeydown" />
</header>
<section class="section-block admin-section">
@@ -492,9 +532,11 @@ async function submitDialog() {
<div v-else class="admin-panels">
<AdminOverview
v-if="!selectedUser && !selectedOrg && (info.is_global_admin || info.is_org_admin)"
ref="adminOverviewRef"
:info="info"
:orgs="orgs"
:permissions="permissions"
:navigation-disabled="hasActiveModal"
:permission-summary="permissionSummary"
@create-org="createOrg"
@open-org="openOrg"
@@ -504,15 +546,18 @@ async function submitDialog() {
@open-dialog="openDialog"
@delete-permission="deletePermission"
@rename-permission-display="renamePermissionDisplay"
@navigate-out="handlePanelNavigateOut"
/>
<AdminUserDetail
v-else-if="selectedUser"
ref="adminUserDetailRef"
:selected-user="selectedUser"
:user-detail="userDetail"
:selected-org="selectedOrg"
:loading="loading"
:show-reg-modal="showRegModal"
:navigation-disabled="hasActiveModal"
@generate-user-registration-link="generateUserRegistrationLink"
@go-overview="goOverview"
@open-org="openOrg"
@@ -520,11 +565,14 @@ async function submitDialog() {
@refresh-user-detail="refreshUserDetail"
@edit-user-name="editUserName"
@close-reg-modal="showRegModal = false"
@navigate-out="handlePanelNavigateOut"
/>
<AdminOrgDetail
v-else-if="selectedOrg"
ref="adminOrgDetailRef"
:selected-org="selectedOrg"
:permissions="permissions"
:navigation-disabled="hasActiveModal"
@update-org="updateOrg"
@create-role="createRole"
@update-role="updateRole"
@@ -533,6 +581,7 @@ async function submitDialog() {
@open-user="openUser"
@toggle-role-permission="toggleRolePermission"
@on-role-drag-over="onRoleDragOver"
@navigate-out="handlePanelNavigateOut"
@on-role-drop="onRoleDrop"
@on-user-drag-start="onUserDragStart"
/>